Refactors Active Record connection management

While the three-tier config makes it easier to define databases for
multiple database applications, it quickly became clear to offer full
support for multiple databases we need to change the way the connections
hash was handled.

A three-tier config means that when Rails needed to choose a default
configuration (in the case a user doesn't ask for a specific
configuration) it wasn't clear to Rails which the default was. I
[bandaid fixed this so the rake tasks could work](#32271) but that fix
wasn't correct because it actually doubled up the configuration hashes.

Instead of attemping to manipulate the hashes @tenderlove and I decided
that it made more sense if we converted the hashes to objects so we can
easily ask those object questions. In a three tier config like this:

```
development:
  primary:
    database: "my_primary_db"
  animals:
    database; "my_animals_db"
```

We end up with an object like this:

```
  @configurations=[
    #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
      @env_name="development",@spec_name="primary",
      @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>,
    #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90
      @env_name="development",@spec_name="animals",
      @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>
]>
```

The configurations setter takes the database configuration set by your
application and turns them into an
`ActiveRecord::DatabaseConfigurations` object that has one getter -
`@configurations` which is an array of all the database objects.

The configurations getter returns this object by default since it acts
like a hash in most of the cases we need. For example if you need to
access the default `development` database we can simply request it as we
did before:

```
ActiveRecord::Base.configurations["development"]
```

This will return primary development database configuration hash:

```
{ "database" => "my_primary_db" }
```

Internally all of Active Record has been converted to use the new
objects. I've built this to be backwards compatible but allow for
accessing the hash if needed for a deprecation period. To get the
original hash instead of the object you can either add `to_h` on the
configurations call or pass `legacy: true` to `configurations.

```
ActiveRecord::Base.configurations.to_h
=> { "development => { "database" => "my_primary_db" } }

ActiveRecord::Base.configurations(legacy: true)
=> { "development => { "database" => "my_primary_db" } }
```

The new configurations object allows us to iterate over the Active
Record configurations without losing the known environment or
specification name for that configuration. You can also select all the
configs for an env or env and spec. With this we can always ask
any object what environment it belongs to:

```
db_configs = ActiveRecord::Base.configurations.configurations_for("development")
=> #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800
  @configurations=[
    #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
      @env_name="development",@spec_name="primary",
      @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>,
    #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90
      @env_name="development",@spec_name="animals",
      @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>
]>

db_config.env_name
=> "development"

db_config.spec_name
=> "primary"

db_config.config
=> { "adapter"=>"sqlite3", "database"=>"db/development.sqlite3" }
```

The configurations object is more flexible than the configurations hash
and will allow us to build on top of the connection management in order
to add support for primary/replica connections, sharding, and
constructing queries for associations that live in multiple databases.
This commit is contained in:
Eileen Uchitelle 2018-08-16 15:49:18 -04:00
parent 3d2caab7dc
commit fdf3f0b930
21 changed files with 1196 additions and 259 deletions

View File

@ -1,3 +1,50 @@
* ActiveRecord::Base.configurations now returns an object.
ActiveRecord::Base.configurations used to return a hash, but this
is an inflexible data model. In order to improve multiple-database
handling in Rails, we've changed this to return an object. Some methods
are provided to make the object behave hash-like in order to ease the
transition process. Since most applications don't manipulate the hash
we've decided to add backwards-compatible functionality that will throw
a deprecation warning if used, however calling `ActiveRecord::Base.configurations`
will use the new version internally and externally.
For example, the following database.yml...
```
development:
adapter: sqlite3
database: db/development.sqlite3
```
Used to become a hash:
```
{ "development" => { "adapter" => "sqlite3", "database" => "db/development.sqlite3" } }
```
Is now converted into the following object:
```
#<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[
#<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
@spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>
]
```
Iterating over the database configurations has also changed. Instead of
calling hash methods on the `configurations` hash directly, a new method `configs_for` has
been provided that allows you to select the correct configuration. `env_name` is a required
argument, `spec_name` is optional as well as passing a block. These return an array of
database config objects for the requested environment and specification name respectively.
```
ActiveRecord::Base.configurations.configs_for("development")
ActiveRecord::Base.configurations.configs_for("development", "primary")
```
*Eileen M. Uchitelle*, *Aaron Patterson*
* Add database configuration to disable advisory locks.
```

View File

@ -40,7 +40,6 @@ module ActiveRecord
autoload :Core
autoload :ConnectionHandling
autoload :CounterCache
autoload :DatabaseConfigurations
autoload :DynamicMatchers
autoload :Enum
autoload :InternalMetadata

View File

@ -22,6 +22,7 @@ require "active_record/explain_subscriber"
require "active_record/relation/delegation"
require "active_record/attributes"
require "active_record/type_caster"
require "active_record/database_configurations"
module ActiveRecord #:nodoc:
# = Active Record
@ -291,7 +292,6 @@ module ActiveRecord #:nodoc:
extend Aggregations::ClassMethods
include Core
include DatabaseConfigurations
include Persistence
include ReadonlyAttributes
include ModelSchema

View File

@ -114,8 +114,7 @@ module ActiveRecord
class Resolver # :nodoc:
attr_reader :configurations
# Accepts a hash two layers deep, keys on the first layer represent
# environments such as "production". Keys must be strings.
# Accepts a list of db config objects.
def initialize(configurations)
@configurations = configurations
end
@ -136,33 +135,14 @@ module ActiveRecord
# Resolver.new(configurations).resolve(:production)
# # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
#
def resolve(config)
if config
resolve_connection config
elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
resolve_symbol_connection env.to_sym
def resolve(config_or_env, pool_name = nil)
if config_or_env
resolve_connection config_or_env, pool_name
else
raise AdapterNotSpecified
end
end
# Expands each key in @configurations hash into fully resolved hash
def resolve_all
config = configurations.dup
if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url"))
end
config.merge! env_config if env_config
config.each do |key, value|
config[key] = resolve(value) if value
end
config
end
# Returns an instance of ConnectionSpecification for a given adapter.
# Accepts a hash one layer deep that contains all connection information.
#
@ -176,7 +156,9 @@ module ActiveRecord
# # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
#
def spec(config)
spec = resolve(config).symbolize_keys
pool_name = config if config.is_a?(Symbol)
spec = resolve(config, pool_name).symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
@ -211,7 +193,6 @@ module ActiveRecord
end
private
# Returns fully resolved connection, accepts hash, string or symbol.
# Always returns a hash.
#
@ -232,29 +213,42 @@ module ActiveRecord
# Resolver.new({}).resolve_connection("postgresql://localhost/foo")
# # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
#
def resolve_connection(spec)
case spec
def resolve_connection(config_or_env, pool_name = nil)
case config_or_env
when Symbol
resolve_symbol_connection spec
resolve_symbol_connection config_or_env, pool_name
when String
resolve_url_connection spec
resolve_url_connection config_or_env
when Hash
resolve_hash_connection spec
resolve_hash_connection config_or_env
else
resolve_connection config_or_env
end
end
# Takes the environment such as +:production+ or +:development+.
# Takes the environment such as +:production+ or +:development+ and a
# pool name the corresponds to the name given by the connection pool
# to the connection. That pool name is merged into the hash with the
# name key.
#
# This requires that the @configurations was initialized with a key that
# matches.
#
# Resolver.new("production" => {}).resolve_symbol_connection(:production)
# # => {}
# configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0
# @configurations=[
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250
# @env_name="production", @spec_name="primary", @config={"database"=>"my_db"}>
# ]>
#
def resolve_symbol_connection(spec)
if config = configurations[spec.to_s]
resolve_connection(config).merge("name" => spec.to_s)
# Resolver.new(configurations).resolve_symbol_connection(:production, "primary")
# # => { "database" => "my_db" }
def resolve_symbol_connection(env_name, pool_name)
db_config = configurations.find_db_config(env_name)
if db_config
resolve_connection(db_config.config).merge("name" => pool_name.to_s)
else
raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}")
raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}")
end
end

View File

@ -46,45 +46,18 @@ module ActiveRecord
#
# The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+
# may be returned on an error.
def establish_connection(config = nil)
def establish_connection(config_or_env = nil)
raise "Anonymous class is not allowed." unless name
config ||= DEFAULT_ENV.call.to_sym
spec_name = self == Base ? "primary" : name
self.connection_specification_name = spec_name
config_or_env ||= DEFAULT_ENV.call.to_sym
pool_name = self == Base ? "primary" : name
self.connection_specification_name = pool_name
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
spec = resolver.resolve(config).symbolize_keys
spec[:name] = spec_name
config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys
config_hash[:name] = pool_name
# use the primary config if a config is not passed in and
# it's a three tier config
spec = spec[spec_name.to_sym] if spec[spec_name.to_sym]
connection_handler.establish_connection(spec)
end
class MergeAndResolveDefaultUrlConfig # :nodoc:
def initialize(raw_configurations)
@raw_config = raw_configurations.dup
@env = DEFAULT_ENV.call.to_s
end
# Returns fully resolved connection hashes.
# Merges connection information from `ENV['DATABASE_URL']` if available.
def resolve
ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all
end
private
def config
@raw_config.dup.tap do |cfg|
if url = ENV["DATABASE_URL"]
cfg[@env] ||= {}
cfg[@env]["url"] ||= url
end
end
end
connection_handler.establish_connection(config_hash)
end
# Returns the connection currently associated with the class. This can

View File

@ -26,7 +26,7 @@ module ActiveRecord
##
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
# as an ActiveRecord::DatabaseConfigurations object.
#
# For example, the following database.yml...
#
@ -40,22 +40,18 @@ module ActiveRecord
#
# ...would result in ActiveRecord::Base.configurations to look like this:
#
# {
# 'development' => {
# 'adapter' => 'sqlite3',
# 'database' => 'db/development.sqlite3'
# },
# 'production' => {
# 'adapter' => 'sqlite3',
# 'database' => 'db/production.sqlite3'
# }
# }
# #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
# @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>,
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production",
# @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}>
# ]>
def self.configurations=(config)
@@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
@@configurations = ActiveRecord::DatabaseConfigurations.new(config)
end
self.configurations = {}
# Returns fully resolved configurations hash
# Returns fully resolved ActiveRecord::DatabaseConfigurations object
def self.configurations
@@configurations
end

View File

@ -1,50 +1,129 @@
# frozen_string_literal: true
require "active_record/database_configurations/database_config"
require "active_record/database_configurations/hash_config"
require "active_record/database_configurations/url_config"
module ActiveRecord
module DatabaseConfigurations # :nodoc:
class DatabaseConfig
attr_reader :env_name, :spec_name, :config
# ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
# objects (either a HashConfig or UrlConfig) that are constructed from the
# application's database configuration hash or url string.
class DatabaseConfigurations
attr_reader :configurations
def initialize(env_name, spec_name, config)
@env_name = env_name
@spec_name = spec_name
@config = config
end
def initialize(configurations = {})
@configurations = build_configs(configurations)
end
# Selects the config for the specified environment and specification name
# Collects the configs for the environment and optionally the specification
# name passed in.
#
# 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
# If a spec name is provided a single DatabaseConfiguration object will be
# returned, otherwise an array of DatabaseConfiguration objects will be
# returned that corresponds with the environment requested.
def configs_for(env = nil, spec = nil, &blk)
configs = env_with_configs(env)
# 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
if spec
configs.find do |db_config|
db_config.spec_name == spec
end
else
env_with_configs
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"] || config["url"] || config["adapter"]
DatabaseConfig.new(env_name, spec_name, config)
# Returns the config hash that corresponds with the environment
#
# If the application has multiple databases `default_hash` will
# the first config hash for the environment.
#
# { database: "my_db", adapter: "mysql2" }
def default_hash(env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s)
default = find_db_config(env)
default.config if default
end
alias :[] :default_hash
# Returns a single DatabaseConfig object based on the requested environment.
#
# If the application has multiple databases `select_db_config` will return
# the first DatabaseConfig for the environment.
def find_db_config(env)
configurations.find do |db_config|
db_config.env_name == env.to_s ||
(db_config.for_current_env? && db_config.spec_name == env.to_s)
end
end
# Returns the DatabaseConfig object as a Hash.
def to_h
configs = configurations.reverse.inject({}) do |memo, db_config|
memo.merge(db_config.to_legacy_hash)
end
Hash[configs.to_a.reverse]
end
# Checks if the application's configurations are empty.
#
# Aliased to blank?
def empty?
configurations.empty?
end
alias :blank? :empty?
private
def env_with_configs(env = nil)
if env
configurations.select { |db_config| db_config.env_name == env }
else
configurations
end
end
def build_configs(configs)
return configs.configurations if configs.is_a?(DatabaseConfigurations)
build_db_config = configs.each_pair.flat_map do |env_name, config|
walk_configs(env_name, "primary", config)
end.compact
if url = ENV["DATABASE_URL"]
build_url_config(url, build_db_config)
else
build_db_config
end
end
def walk_configs(env_name, spec_name, config)
case config
when String
build_db_config_from_string(env_name, spec_name, config)
when Hash
build_db_config_from_hash(env_name, spec_name, config)
end
end
def build_db_config_from_string(env_name, spec_name, config)
begin
url = config
uri = URI.parse(url)
if uri.try(:scheme)
ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url)
end
rescue URI::InvalidURIError
ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
end
end
def build_db_config_from_hash(env_name, spec_name, config)
if url = config["url"]
config_without_url = config.dup
config_without_url.delete "url"
ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
else
config.each_pair.map do |sub_spec_name, sub_config|
walk_configs(env_name, sub_spec_name, sub_config)
@ -52,11 +131,37 @@ module ActiveRecord
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)
def build_url_config(url, configs)
env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
if original_config = configs.find(&:for_current_env?)
if original_config.url_config?
configs
else
configs.map do |config|
ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config)
end
end
else
configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)]
end
end
def method_missing(method, *args, &blk)
if Hash.method_defined?(method)
ActiveSupport::Deprecation.warn \
"Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations."
end
case method
when :each, :first
configurations.send(method, *args, &blk)
when :fetch
configs_for(args.first)
when :values
configurations.map(&:config)
else
super
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module ActiveRecord
class DatabaseConfigurations
# ActiveRecord::Base.configurations will return either a HashConfig or
# UrlConfig respectively. It will never return a DatabaseConfig object,
# as this is the parent class for the types of database configuration objects.
class DatabaseConfig # :nodoc:
attr_reader :env_name, :spec_name
def initialize(env_name, spec_name)
@env_name = env_name
@spec_name = spec_name
end
def url_config?
false
end
def to_legacy_hash
{ env_name => config }
end
def for_current_env?
env_name == ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module ActiveRecord
class DatabaseConfigurations
# A HashConfig object is created for each database configuration entry that
# is created from a hash.
#
# A hash config:
#
# { "development" => { "database" => "db_name" } }
#
# Becomes:
#
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
# @env_name="development", @spec_name="primary", @config={"db_name"}>
#
# Options are:
#
# <tt>:env_name</tt> - The Rails environment, ie "development"
# <tt>:spec_name</tt> - The specification name. In a standard two-tier
# database configuration this will default to "primary". In a multiple
# database three-tier database configuration this corresponds to the name
# used in the second tier, for example "primary_readonly".
# <tt>:config</tt> - The config hash. This is the hash that contains the
# database adapter, name, and other important information for database
# connections.
class HashConfig < DatabaseConfig
attr_reader :config
def initialize(env_name, spec_name, config)
super(env_name, spec_name)
@config = config
end
end
end
end

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
module ActiveRecord
class DatabaseConfigurations
# A UrlConfig object is created for each database configuration
# entry that is created from a URL. This can either be a URL string
# or a hash with a URL in place of the config hash.
#
# A URL config:
#
# postgres://localhost/foo
#
# Becomes:
#
# #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340
# @env_name="default_env", @spec_name="primary",
# @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"},
# @url="postgres://localhost/foo">
#
# Options are:
#
# <tt>:env_name</tt> - The Rails environment, ie "development"
# <tt>:spec_name</tt> - The specification name. In a standard two-tier
# database configuration this will default to "primary". In a multiple
# database three-tier database configuration this corresponds to the name
# used in the second tier, for example "primary_readonly".
# <tt>:url</tt> - The database URL.
# <tt>:config</tt> - The config hash. This is the hash that contains the
# database adapter, name, and other important information for database
# connections.
class UrlConfig < DatabaseConfig
attr_reader :url, :config
def initialize(env_name, spec_name, url, config = {})
super(env_name, spec_name)
@config = build_config(config, url)
@url = url
end
def url_config? # :nodoc:
true
end
private
def build_config(original_config, url)
if /^jdbc:/.match?(url)
hash = { "url" => url }
else
hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
end
if original_config[env_name]
original_config[env_name].merge(hash)
else
original_config.merge(hash)
end
end
end
end
end

View File

@ -26,7 +26,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
desc "Create #{spec_name} database for current environment"
task spec_name => :load_config do
db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
db_config = ActiveRecord::Base.configurations.configs_for(Rails.env, spec_name)
ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
end
end
@ -45,7 +45,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
desc "Drop #{spec_name} database for current environment"
task spec_name => [:load_config, :check_protected_environments] do
db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
db_config = ActiveRecord::Base.configurations.configs_for(Rails.env, spec_name)
ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
end
end
@ -73,8 +73,8 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task migrate: :load_config do
ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
ActiveRecord::Base.establish_connection(config)
ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate
end
db_namespace["_dump"].invoke
@ -99,7 +99,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
desc "Migrate #{spec_name} database for current environment"
task spec_name => :load_config do
db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
db_config = ActiveRecord::Base.configurations.configs_for(Rails.env, spec_name)
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate
end
@ -274,11 +274,10 @@ 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"
ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby)
ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config|
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
File.open(filename, "w:utf-8") do |file|
ActiveRecord::Base.establish_connection(config)
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
end
@ -314,11 +313,10 @@ 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::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
ActiveRecord::Base.establish_connection(config)
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename)
ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename)
if ActiveRecord::SchemaMigration.table_exists?
File.open(filename, "a") do |f|
f.puts ActiveRecord::Base.connection.dump_schema_information
@ -356,22 +354,30 @@ db_namespace = namespace :db do
begin
should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Schema.verbose = false
ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test"
ActiveRecord::Base.configurations.configs_for("test").each do |db_config|
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test")
end
ensure
if should_reconnect
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env))
end
end
end
# desc "Recreate the test database from an existent structure.sql file"
task load_structure: %w(db:test:purge) do
ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test"
ActiveRecord::Base.configurations.configs_for("test").each do |db_config|
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql)
ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :sql, filename, "test")
end
end
# desc "Empty the test database"
task purge: %w(load_config check_protected_environments) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"]
ActiveRecord::Base.configurations.configs_for("test").each do |db_config|
ActiveRecord::Tasks::DatabaseTasks.purge(db_config.config)
end
end
# desc 'Load the test schema'

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require "active_record/database_configurations"
module ActiveRecord
module Tasks # :nodoc:
class DatabaseAlreadyExists < StandardError; end # :nodoc:
@ -101,16 +103,21 @@ module ActiveRecord
@env ||= Rails.env
end
def spec
@spec ||= "primary"
end
def seed_loader
@seed_loader ||= Rails.application
end
def current_config(options = {})
options.reverse_merge! env: env
options[:spec] ||= "primary"
if options.has_key?(:config)
@current_config = options[:config]
else
@current_config ||= ActiveRecord::Base.configurations[options[:env]]
@current_config ||= ActiveRecord::Base.configurations.configs_for(options[:env], options[:spec]).config
end
end
@ -136,7 +143,7 @@ module ActiveRecord
def for_each
databases = Rails.application.config.database_configuration
database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases)
database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(Rails.env)
# if this is a single database application we don't want tasks for each primary database
return if database_configs.count == 1
@ -180,9 +187,11 @@ module ActiveRecord
scope = ENV["SCOPE"]
verbose_was, Migration.verbose = Migration.verbose, verbose?
Base.connection.migration_context.migrate(target_version) do |migration|
scope.blank? || scope == migration.scope
end
ActiveRecord::Base.clear_cache!
ensure
Migration.verbose = verbose_was
@ -198,8 +207,8 @@ module ActiveRecord
ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
end
def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
def charset_current(environment = env, specification_name = spec)
charset ActiveRecord::Base.configurations.configs_for(environment, specification_name).config
end
def charset(*arguments)
@ -207,8 +216,8 @@ module ActiveRecord
class_for_adapter(configuration["adapter"]).new(*arguments).charset
end
def collation_current(environment = env)
collation ActiveRecord::Base.configurations[environment]
def collation_current(environment = env, specification_name = spec)
collation ActiveRecord::Base.configurations.configs_for(environment, specification_name).config
end
def collation(*arguments)
@ -342,14 +351,15 @@ module ActiveRecord
environments << "test" if environment == "development"
environments.each do |env|
ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration|
yield configuration, spec_name, env
ActiveRecord::Base.configurations.configs_for(env).each do |db_config|
yield db_config.config, db_config.spec_name, env
end
end
end
def each_local_configuration
ActiveRecord::Base.configurations.each_value do |configuration|
ActiveRecord::Base.configurations.configs_for.each do |db_config|
configuration = db_config.config
next unless configuration["database"]
if local_database?(configuration)

View File

@ -5,31 +5,32 @@ require "active_support/testing/parallelization"
module ActiveRecord
module TestDatabases # :nodoc:
ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
create_and_load_schema(i, spec_name: Rails.env)
create_and_load_schema(i, env_name: Rails.env)
end
ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_|
drop(spec_name: Rails.env)
ActiveSupport::Testing::Parallelization.run_cleanup_hook do
drop(env_name: Rails.env)
end
def self.create_and_load_schema(i, spec_name:)
def self.create_and_load_schema(i, env_name:)
old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
connection_spec = ActiveRecord::Base.configurations[spec_name]
connection_spec["database"] += "-#{i}"
ActiveRecord::Tasks::DatabaseTasks.create(connection_spec)
ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec)
ActiveRecord::Base.configurations.configs_for(env_name).each do |db_config|
db_config.config["database"] += "-#{i}"
ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, ActiveRecord::Base.schema_format, nil, env_name, db_config.spec_name)
end
ensure
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
ActiveRecord::Base.establish_connection(Rails.env.to_sym)
ENV["VERBOSE"] = old
end
def self.drop(spec_name:)
def self.drop(env_name:)
old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
connection_spec = ActiveRecord::Base.configurations[spec_name]
ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec)
ActiveRecord::Base.configurations.configs_for(env_name).each do |db_config|
ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
end
ensure
ENV["VERBOSE"] = old
end

View File

@ -28,13 +28,16 @@ module ActiveRecord
end
def test_establish_connection_uses_spec_name
old_config = ActiveRecord::Base.configurations
config = { "readonly" => { "adapter" => "sqlite3" } }
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config)
ActiveRecord::Base.configurations = config
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations)
spec = resolver.spec(:readonly)
@handler.establish_connection(spec.to_hash)
assert_not_nil @handler.retrieve_connection_pool("readonly")
ensure
ActiveRecord::Base.configurations = old_config
@handler.remove_connection("readonly")
end

View File

@ -18,11 +18,14 @@ module ActiveRecord
end
def resolve_config(config)
ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
configs = ActiveRecord::DatabaseConfigurations.new(config)
configs.to_h
end
def resolve_spec(spec, config)
ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec)
configs = ActiveRecord::DatabaseConfigurations.new(config)
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
resolver.resolve(spec, spec)
end
def test_resolver_with_database_uri_and_current_env_symbol_key

View File

@ -7,11 +7,15 @@ module ActiveRecord
class ConnectionSpecification
class ResolverTest < ActiveRecord::TestCase
def resolve(spec, config = {})
Resolver.new(config).resolve(spec)
configs = ActiveRecord::DatabaseConfigurations.new(config)
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
resolver.resolve(spec, spec)
end
def spec(spec, config = {})
Resolver.new(config).spec(spec)
configs = ActiveRecord::DatabaseConfigurations.new(config)
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
resolver.spec(spec)
end
def test_url_invalid_adapter

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
require "cases/helper"
module ActiveRecord
class LegacyConfigurationsTest < ActiveRecord::TestCase
def test_can_turn_configurations_into_a_hash
assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not."
assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort
end
def test_each_is_deprecated
assert_deprecated do
ActiveRecord::Base.configurations.each do |db_config|
assert_equal "primary", db_config.spec_name
end
end
end
def test_first_is_deprecated
assert_deprecated do
db_config = ActiveRecord::Base.configurations.first
assert_equal "arunit", db_config.env_name
assert_equal "primary", db_config.spec_name
end
end
def test_fetch_is_deprecated
assert_deprecated do
db_config = ActiveRecord::Base.configurations.fetch("arunit").first
assert_equal "arunit", db_config.env_name
assert_equal "primary", db_config.spec_name
end
end
def test_values_are_deprecated
config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config)
assert_deprecated do
assert_equal config_hashes, ActiveRecord::Base.configurations.values
end
end
end
end

View File

@ -156,20 +156,24 @@ module ActiveRecord
class DatabaseTasksCreateAllTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations
@configurations = { "development" => { "database" => "my-db" } }
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
ActiveRecord::Base.configurations = @configurations
end
def teardown
$stdout, $stderr = @original_stdout, @original_stderr
ActiveRecord::Base.configurations = @old_configurations
end
def test_ignores_configurations_without_databases
@configurations["development"]["database"] = nil
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
@ -179,7 +183,7 @@ module ActiveRecord
def test_ignores_remote_databases
@configurations["development"]["host"] = "my.server.tld"
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
@ -189,7 +193,7 @@ module ActiveRecord
def test_warning_for_remote_databases
@configurations["development"]["host"] = "my.server.tld"
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
ActiveRecord::Tasks::DatabaseTasks.create_all
assert_match "This task only modifies local databases. my-db is on a remote host.",
@ -200,7 +204,7 @@ module ActiveRecord
def test_creates_configurations_with_local_ip
@configurations["development"]["host"] = "127.0.0.1"
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
@ -210,7 +214,7 @@ module ActiveRecord
def test_creates_configurations_with_local_host
@configurations["development"]["host"] = "localhost"
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
@ -220,40 +224,33 @@ module ActiveRecord
def test_creates_configurations_with_blank_hosts
@configurations["development"]["host"] = nil
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
end
end
private
def with_stubbed_configurations_establish_connection
ActiveRecord::Base.stub(:configurations, @configurations) do
# To refrain from connecting to a newly created empty DB in
# sqlite3_mem tests
ActiveRecord::Base.connection_handler.stub(
:establish_connection,
nil
) do
yield
end
end
end
end
class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
"production" => { "url" => "prod-db-url" }
"production" => { "url" => "abstract://prod-db-url" }
}
ActiveRecord::Base.configurations = @configurations
end
def teardown
ActiveRecord::Base.configurations = @old_configurations
end
def test_creates_current_environment_database
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
@ -267,7 +264,7 @@ module ActiveRecord
end
def test_creates_current_environment_database_with_url
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
@ -281,7 +278,7 @@ module ActiveRecord
end
def test_creates_test_and_development_databases_when_env_was_not_specified
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
@ -301,7 +298,7 @@ module ActiveRecord
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
@ -328,29 +325,27 @@ module ActiveRecord
end
end
end
private
def with_stubbed_configurations_establish_connection
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.stub(:establish_connection, nil) do
yield
end
end
end
end
class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations
@configurations = {
"development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
"test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
"production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } }
"production" => { "primary" => { "url" => "abstract://prod-db-url" }, "secondary" => { "url" => "abstract://secondary-prod-db-url" } }
}
ActiveRecord::Base.configurations = @configurations
end
def teardown
ActiveRecord::Base.configurations = @old_configurations
end
def test_creates_current_environment_database
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
@ -367,7 +362,7 @@ module ActiveRecord
end
def test_creates_current_environment_database_with_url
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
@ -384,7 +379,7 @@ module ActiveRecord
end
def test_creates_test_and_development_databases_when_env_was_not_specified
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
@ -406,7 +401,7 @@ module ActiveRecord
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
with_stubbed_configurations_establish_connection do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
@ -439,16 +434,6 @@ module ActiveRecord
end
end
end
private
def with_stubbed_configurations_establish_connection
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.stub(:establish_connection, nil) do
yield
end
end
end
end
class DatabaseTasksDropTest < ActiveRecord::TestCase
@ -467,20 +452,24 @@ module ActiveRecord
class DatabaseTasksDropAllTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations
@configurations = { development: { "database" => "my-db" } }
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
ActiveRecord::Base.configurations = @configurations
end
def teardown
$stdout, $stderr = @original_stdout, @original_stderr
ActiveRecord::Base.configurations = @old_configurations
end
def test_ignores_configurations_without_databases
@configurations[:development]["database"] = nil
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
@ -490,7 +479,7 @@ module ActiveRecord
def test_ignores_remote_databases
@configurations[:development]["host"] = "my.server.tld"
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
@ -500,7 +489,7 @@ module ActiveRecord
def test_warning_for_remote_databases
@configurations[:development]["host"] = "my.server.tld"
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
ActiveRecord::Tasks::DatabaseTasks.drop_all
assert_match "This task only modifies local databases. my-db is on a remote host.",
@ -511,7 +500,7 @@ module ActiveRecord
def test_drops_configurations_with_local_ip
@configurations[:development]["host"] = "127.0.0.1"
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
@ -521,7 +510,7 @@ module ActiveRecord
def test_drops_configurations_with_local_host
@configurations[:development]["host"] = "localhost"
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
@ -531,7 +520,7 @@ module ActiveRecord
def test_drops_configurations_with_blank_hosts
@configurations[:development]["host"] = nil
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
@ -541,15 +530,22 @@ module ActiveRecord
class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
"production" => { "url" => "prod-db-url" }
"production" => { "url" => "abstract://prod-db-url" }
}
ActiveRecord::Base.configurations = @configurations
end
def teardown
ActiveRecord::Base.configurations = @old_configurations
end
def test_drops_current_environment_database
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop,
["database" => "test-db"]) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
@ -560,7 +556,7 @@ module ActiveRecord
end
def test_drops_current_environment_database_with_url
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop,
["url" => "prod-db-url"]) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
@ -571,7 +567,7 @@ module ActiveRecord
end
def test_drops_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
@ -591,7 +587,7 @@ module ActiveRecord
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
@ -612,15 +608,22 @@ module ActiveRecord
class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations
@configurations = {
"development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
"test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
"production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } }
"production" => { "primary" => { "url" => "abstract://prod-db-url" }, "secondary" => { "url" => "abstract://secondary-prod-db-url" } }
}
ActiveRecord::Base.configurations = @configurations
end
def teardown
ActiveRecord::Base.configurations = @old_configurations
end
def test_drops_current_environment_database
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
@ -637,7 +640,7 @@ module ActiveRecord
end
def test_drops_current_environment_database_with_url
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
@ -654,7 +657,7 @@ module ActiveRecord
end
def test_drops_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
@ -676,7 +679,7 @@ module ActiveRecord
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
ActiveRecord::Base.stub(:configurations, @configurations) do
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
@ -848,12 +851,16 @@ module ActiveRecord
class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase
def test_purges_current_environment_database
old_configurations = ActiveRecord::Base.configurations
configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
"production" => { "database" => "prod-db" }
}
ActiveRecord::Base.stub(:configurations, configurations) do
ActiveRecord::Base.configurations = configurations
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:purge,
@ -864,13 +871,17 @@ module ActiveRecord
end
end
end
ensure
ActiveRecord::Base.configurations = old_configurations
end
end
class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase
def test_purge_all_local_configurations
old_configurations = ActiveRecord::Base.configurations
configurations = { development: { "database" => "my-db" } }
ActiveRecord::Base.stub(:configurations, configurations) do
ActiveRecord::Base.configurations = configurations
ActiveRecord::Base.configurations do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:purge,
@ -879,6 +890,8 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.purge_all
end
end
ensure
ActiveRecord::Base.configurations = old_configurations
end
end

View File

@ -0,0 +1,591 @@
# frozen_string_literal: true
require "cases/helper"
require "active_record/tasks/database_tasks"
module ActiveRecord
class LegacyDatabaseTasksCreateAllTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations.to_h
@configurations = { "development" => { "database" => "my-db" } }
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
ActiveRecord::Base.configurations = @configurations
end
def teardown
$stdout, $stderr = @original_stdout, @original_stderr
ActiveRecord::Base.configurations = @old_configurations
end
def test_ignores_configurations_without_databases
@configurations["development"]["database"] = nil
ActiveRecord::Base.configurations.to_h do
assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
end
end
def test_ignores_remote_databases
@configurations["development"]["host"] = "my.server.tld"
ActiveRecord::Base.configurations.to_h do
assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
end
end
def test_warning_for_remote_databases
@configurations["development"]["host"] = "my.server.tld"
ActiveRecord::Base.configurations.to_h do
ActiveRecord::Tasks::DatabaseTasks.create_all
assert_match "This task only modifies local databases. my-db is on a remote host.",
$stderr.string
end
end
def test_creates_configurations_with_local_ip
@configurations["development"]["host"] = "127.0.0.1"
ActiveRecord::Base.configurations.to_h do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
end
end
def test_creates_configurations_with_local_host
@configurations["development"]["host"] = "localhost"
ActiveRecord::Base.configurations.to_h do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
end
end
def test_creates_configurations_with_blank_hosts
@configurations["development"]["host"] = nil
ActiveRecord::Base.configurations.to_h do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
end
end
end
class LegacyDatabaseTasksCreateCurrentTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations.to_h
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
"production" => { "url" => "abstract://prod-db-url" }
}
ActiveRecord::Base.configurations = @configurations
end
def teardown
ActiveRecord::Base.configurations = @old_configurations
end
def test_creates_current_environment_database
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
["database" => "test-db"],
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("test")
)
end
end
end
def test_creates_current_environment_database_with_url
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
["url" => "prod-db-url"],
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("production")
)
end
end
end
def test_creates_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
[
["database" => "dev-db"],
["database" => "test-db"]
],
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
end
def test_creates_test_and_development_databases_when_rails_env_is_development
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
[
["database" => "dev-db"],
["database" => "test-db"]
],
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
ensure
ENV["RAILS_ENV"] = old_env
end
def test_establishes_connection_for_the_given_environments
ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do
assert_called_with(ActiveRecord::Base, :establish_connection, [:development]) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
end
end
class LegacyDatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations.to_h
@configurations = {
"development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
"test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
"production" => { "primary" => { "url" => "abstract://prod-db-url" }, "secondary" => { "url" => "abstract://secondary-prod-db-url" } }
}
ActiveRecord::Base.configurations = @configurations
end
def teardown
ActiveRecord::Base.configurations = @old_configurations
end
def test_creates_current_environment_database
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
[
["database" => "test-db"],
["database" => "secondary-test-db"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("test")
)
end
end
end
def test_creates_current_environment_database_with_url
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
[
["url" => "prod-db-url"],
["url" => "secondary-prod-db-url"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("production")
)
end
end
end
def test_creates_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
[
["database" => "dev-db"],
["database" => "secondary-dev-db"],
["database" => "test-db"],
["database" => "secondary-test-db"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
end
def test_creates_test_and_development_databases_when_rails_env_is_development
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:create,
[
["database" => "dev-db"],
["database" => "secondary-dev-db"],
["database" => "test-db"],
["database" => "secondary-test-db"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
ensure
ENV["RAILS_ENV"] = old_env
end
def test_establishes_connection_for_the_given_environments_config
ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do
assert_called_with(
ActiveRecord::Base,
:establish_connection,
[:development]
) do
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
end
end
class LegacyDatabaseTasksDropAllTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations.to_h
@configurations = { development: { "database" => "my-db" } }
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
ActiveRecord::Base.configurations = @configurations
end
def teardown
$stdout, $stderr = @original_stdout, @original_stderr
ActiveRecord::Base.configurations = @old_configurations
end
def test_ignores_configurations_without_databases
@configurations[:development]["database"] = nil
ActiveRecord::Base.configurations.to_h do
assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
end
end
def test_ignores_remote_databases
@configurations[:development]["host"] = "my.server.tld"
ActiveRecord::Base.configurations.to_h do
assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
end
end
def test_warning_for_remote_databases
@configurations[:development]["host"] = "my.server.tld"
ActiveRecord::Base.configurations.to_h do
ActiveRecord::Tasks::DatabaseTasks.drop_all
assert_match "This task only modifies local databases. my-db is on a remote host.",
$stderr.string
end
end
def test_drops_configurations_with_local_ip
@configurations[:development]["host"] = "127.0.0.1"
ActiveRecord::Base.configurations.to_h do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
end
end
def test_drops_configurations_with_local_host
@configurations[:development]["host"] = "localhost"
ActiveRecord::Base.configurations.to_h do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
end
end
def test_drops_configurations_with_blank_hosts
@configurations[:development]["host"] = nil
ActiveRecord::Base.configurations.to_h do
assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
end
end
end
class LegacyDatabaseTasksDropCurrentTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations.to_h
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
"production" => { "url" => "abstract://prod-db-url" }
}
ActiveRecord::Base.configurations = @configurations
end
def teardown
ActiveRecord::Base.configurations = @old_configurations
end
def test_drops_current_environment_database
ActiveRecord::Base.configurations.to_h do
assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop,
["database" => "test-db"]) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("test")
)
end
end
end
def test_drops_current_environment_database_with_url
ActiveRecord::Base.configurations.to_h do
assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop,
["url" => "prod-db-url"]) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("production")
)
end
end
end
def test_drops_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
[
["database" => "dev-db"],
["database" => "test-db"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
end
def test_drops_testand_development_databases_when_rails_env_is_development
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
[
["database" => "dev-db"],
["database" => "test-db"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
ensure
ENV["RAILS_ENV"] = old_env
end
end
class LegacyDatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase
def setup
@old_configurations = ActiveRecord::Base.configurations.to_h
@configurations = {
"development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
"test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
"production" => { "primary" => { "url" => "abstract://prod-db-url" }, "secondary" => { "url" => "abstract://secondary-prod-db-url" } }
}
ActiveRecord::Base.configurations = @configurations
end
def teardown
ActiveRecord::Base.configurations = @old_configurations
end
def test_drops_current_environment_database
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
[
["database" => "test-db"],
["database" => "secondary-test-db"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("test")
)
end
end
end
def test_drops_current_environment_database_with_url
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
[
["url" => "prod-db-url"],
["url" => "secondary-prod-db-url"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("production")
)
end
end
end
def test_drops_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
[
["database" => "dev-db"],
["database" => "secondary-dev-db"],
["database" => "test-db"],
["database" => "secondary-test-db"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
end
def test_drops_testand_development_databases_when_rails_env_is_development
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:drop,
[
["database" => "dev-db"],
["database" => "secondary-dev-db"],
["database" => "test-db"],
["database" => "secondary-test-db"]
]
) do
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("development")
)
end
end
ensure
ENV["RAILS_ENV"] = old_env
end
end
class LegacyDatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase
def test_purges_current_environment_database
@old_configurations = ActiveRecord::Base.configurations.to_h
configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
"production" => { "database" => "prod-db" }
}
ActiveRecord::Base.configurations = configurations
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:purge,
["database" => "prod-db"]
) do
assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do
ActiveRecord::Tasks::DatabaseTasks.purge_current("production")
end
end
end
ensure
ActiveRecord::Base.configurations = @old_configurations
end
end
class LegacyDatabaseTasksPurgeAllTest < ActiveRecord::TestCase
def test_purge_all_local_configurations
@old_configurations = ActiveRecord::Base.configurations.to_h
configurations = { development: { "database" => "my-db" } }
ActiveRecord::Base.configurations = configurations
ActiveRecord::Base.configurations.to_h do
assert_called_with(
ActiveRecord::Tasks::DatabaseTasks,
:purge,
["database" => "my-db"]
) do
ActiveRecord::Tasks::DatabaseTasks.purge_all
end
end
ensure
ActiveRecord::Base.configurations = @old_configurations
end
end
end

View File

@ -908,7 +908,15 @@ $ echo $DATABASE_URL
postgresql://localhost/my_database
$ rails runner 'puts ActiveRecord::Base.configurations'
{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}}
#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28>
$ rails runner 'puts ActiveRecord::Base.configurations.inspect'
#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[
#<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0
@env_name="development", @spec_name="primary",
@config={"adapter"=>"postgresql", "database"=>"my_database", "host"=>"localhost"}
@url="postgresql://localhost/my_database">
]
```
Here the adapter, host, and database match the information in `ENV['DATABASE_URL']`.
@ -925,7 +933,15 @@ $ echo $DATABASE_URL
postgresql://localhost/my_database
$ rails runner 'puts ActiveRecord::Base.configurations'
{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}}
#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28>
$ rails runner 'puts ActiveRecord::Base.configurations.inspect'
#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[
#<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0
@env_name="development", @spec_name="primary",
@config={"adapter"=>"postgresql", "database"=>"my_database", "host"=>"localhost", "pool"=>5}
@url="postgresql://localhost/my_database">
]
```
Since pool is not in the `ENV['DATABASE_URL']` provided connection information its information is merged in. Since `adapter` is duplicate, the `ENV['DATABASE_URL']` connection information wins.
@ -935,13 +951,21 @@ The only way to explicitly not use the connection information in `ENV['DATABASE_
```
$ cat config/database.yml
development:
url: sqlite3:NOT_my_database
url: sqlite3://NOT_my_database
$ echo $DATABASE_URL
postgresql://localhost/my_database
$ rails runner 'puts ActiveRecord::Base.configurations'
{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}}
#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28>
$ rails runner 'puts ActiveRecord::Base.configurations.inspect'
#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[
#<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0
@env_name="development", @spec_name="primary",
@config={"adapter"=>"sqlite3", "database"=>"NOT_my_database", "host"=>"localhost"}
@url="sqlite3://NOT_my_database">
]
```
Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name.

View File

@ -127,36 +127,36 @@ EOS
test "db:create and db:drop works on all databases for env" do
require "#{app_path}/config/environment"
ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
db_create_and_drop namespace, config["database"]
ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config|
db_create_and_drop db_config.spec_name, db_config.config["database"]
end
end
test "db:create:namespace and db:drop:namespace works on specified databases" do
require "#{app_path}/config/environment"
ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
db_create_and_drop_namespace namespace, config["database"]
ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config|
db_create_and_drop_namespace db_config.spec_name, db_config.config["database"]
end
end
test "db:migrate and db:schema:dump and db:schema:load works on all databases" do
require "#{app_path}/config/environment"
ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
db_migrate_and_schema_dump_and_load namespace, config["database"], "schema"
ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config|
db_migrate_and_schema_dump_and_load db_config.spec_name, db_config.config["database"], "schema"
end
end
test "db:migrate and db:structure:dump and db:structure:load works on all databases" do
require "#{app_path}/config/environment"
ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
db_migrate_and_schema_dump_and_load namespace, config["database"], "structure"
ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config|
db_migrate_and_schema_dump_and_load db_config.spec_name, db_config.config["database"], "structure"
end
end
test "db:migrate:namespace works" do
require "#{app_path}/config/environment"
ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
db_migrate_namespaced namespace, config["database"]
ActiveRecord::Base.configurations.configs_for(Rails.env).each do |db_config|
db_migrate_namespaced db_config.spec_name, db_config.config["database"]
end
end
end