2018-04-06 14:23:59 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require "isolation/abstract_unit"
|
|
|
|
|
|
|
|
module ApplicationTests
|
|
|
|
module RakeTests
|
|
|
|
class RakeMultiDbsTest < ActiveSupport::TestCase
|
|
|
|
include ActiveSupport::Testing::Isolation
|
|
|
|
|
|
|
|
def setup
|
|
|
|
build_app(multi_db: true)
|
|
|
|
FileUtils.rm_rf("#{app_path}/config/environments")
|
|
|
|
end
|
|
|
|
|
|
|
|
def teardown
|
|
|
|
teardown_app
|
|
|
|
end
|
|
|
|
|
2018-08-31 10:55:37 -04:00
|
|
|
def db_create_and_drop(namespace, expected_database)
|
2018-04-06 14:23:59 -04:00
|
|
|
Dir.chdir(app_path) do
|
|
|
|
output = rails("db:create")
|
|
|
|
assert_match(/Created database/, output)
|
|
|
|
assert_match_namespace(namespace, output)
|
2018-08-31 10:55:37 -04:00
|
|
|
assert_no_match(/already exists/, output)
|
2018-04-06 14:23:59 -04:00
|
|
|
assert File.exist?(expected_database)
|
|
|
|
|
|
|
|
output = rails("db:drop")
|
|
|
|
assert_match(/Dropped database/, output)
|
|
|
|
assert_match_namespace(namespace, output)
|
2018-08-31 10:55:37 -04:00
|
|
|
assert_no_match(/does not exist/, output)
|
2018-04-17 18:21:34 -04:00
|
|
|
assert_not File.exist?(expected_database)
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-08-31 10:55:37 -04:00
|
|
|
def db_create_and_drop_namespace(namespace, expected_database)
|
2018-04-06 14:23:59 -04:00
|
|
|
Dir.chdir(app_path) do
|
|
|
|
output = rails("db:create:#{namespace}")
|
|
|
|
assert_match(/Created database/, output)
|
|
|
|
assert_match_namespace(namespace, output)
|
|
|
|
assert File.exist?(expected_database)
|
|
|
|
|
|
|
|
output = rails("db:drop:#{namespace}")
|
|
|
|
assert_match(/Dropped database/, output)
|
|
|
|
assert_match_namespace(namespace, output)
|
2018-04-17 18:21:34 -04:00
|
|
|
assert_not File.exist?(expected_database)
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def assert_match_namespace(namespace, output)
|
|
|
|
if namespace == "primary"
|
|
|
|
assert_match(/#{Rails.env}.sqlite3/, output)
|
|
|
|
else
|
|
|
|
assert_match(/#{Rails.env}_#{namespace}.sqlite3/, output)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-04 15:30:50 -04:00
|
|
|
def db_migrate_and_migrate_status
|
2018-04-06 14:23:59 -04:00
|
|
|
Dir.chdir(app_path) do
|
2018-10-04 15:30:50 -04:00
|
|
|
generate_models_for_animals
|
|
|
|
rails "db:migrate"
|
|
|
|
output = rails "db:migrate:status"
|
|
|
|
assert_match(/up \d+ Create books/, output)
|
|
|
|
assert_match(/up \d+ Create dogs/, output)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-05 16:06:10 -04:00
|
|
|
def db_migrate_and_schema_cache_dump
|
|
|
|
Dir.chdir(app_path) do
|
|
|
|
generate_models_for_animals
|
|
|
|
rails "db:migrate"
|
|
|
|
rails "db:schema:cache:dump"
|
|
|
|
assert File.exist?("db/schema_cache.yml")
|
|
|
|
assert File.exist?("db/animals_schema_cache.yml")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def db_migrate_and_schema_cache_dump_and_schema_cache_clear
|
|
|
|
Dir.chdir(app_path) do
|
|
|
|
generate_models_for_animals
|
|
|
|
rails "db:migrate"
|
|
|
|
rails "db:schema:cache:dump"
|
|
|
|
rails "db:schema:cache:clear"
|
|
|
|
assert_not File.exist?("db/schema_cache.yml")
|
|
|
|
assert_not File.exist?("db/animals_schema_cache.yml")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-04 15:30:50 -04:00
|
|
|
def db_migrate_and_schema_dump_and_load(format)
|
|
|
|
Dir.chdir(app_path) do
|
|
|
|
generate_models_for_animals
|
2018-04-06 14:23:59 -04:00
|
|
|
rails "db:migrate", "db:#{format}:dump"
|
|
|
|
|
|
|
|
if format == "schema"
|
|
|
|
schema_dump = File.read("db/#{format}.rb")
|
|
|
|
schema_dump_animals = File.read("db/animals_#{format}.rb")
|
|
|
|
assert_match(/create_table \"books\"/, schema_dump)
|
|
|
|
assert_match(/create_table \"dogs\"/, schema_dump_animals)
|
|
|
|
else
|
|
|
|
schema_dump = File.read("db/#{format}.sql")
|
|
|
|
schema_dump_animals = File.read("db/animals_#{format}.sql")
|
2018-04-16 16:17:45 -04:00
|
|
|
assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, schema_dump)
|
|
|
|
assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"dogs\"/, schema_dump_animals)
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
rails "db:#{format}:load"
|
|
|
|
|
|
|
|
ar_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
|
|
|
|
animals_tables = lambda { rails("runner", "p AnimalsBase.connection.tables").strip }
|
|
|
|
|
|
|
|
assert_equal '["schema_migrations", "ar_internal_metadata", "books"]', ar_tables[]
|
|
|
|
assert_equal '["schema_migrations", "ar_internal_metadata", "dogs"]', animals_tables[]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-11-08 09:03:44 -05:00
|
|
|
def db_migrate_namespaced(namespace)
|
2018-04-06 14:23:59 -04:00
|
|
|
Dir.chdir(app_path) do
|
2018-10-04 15:30:50 -04:00
|
|
|
generate_models_for_animals
|
2018-04-06 14:23:59 -04:00
|
|
|
output = rails("db:migrate:#{namespace}")
|
|
|
|
if namespace == "primary"
|
|
|
|
assert_match(/CreateBooks: migrated/, output)
|
|
|
|
else
|
|
|
|
assert_match(/CreateDogs: migrated/, output)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-11-08 09:03:44 -05:00
|
|
|
def db_migrate_status_namespaced(namespace)
|
2018-10-04 15:30:50 -04:00
|
|
|
Dir.chdir(app_path) do
|
|
|
|
generate_models_for_animals
|
|
|
|
output = rails("db:migrate:status:#{namespace}")
|
|
|
|
if namespace == "primary"
|
|
|
|
assert_match(/up \d+ Create books/, output)
|
|
|
|
else
|
|
|
|
assert_match(/up \d+ Create dogs/, output)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-08 15:44:51 -04:00
|
|
|
def db_up_and_down(version, namespace = nil)
|
|
|
|
Dir.chdir(app_path) do
|
|
|
|
generate_models_for_animals
|
|
|
|
rails("db:migrate")
|
|
|
|
|
|
|
|
if namespace
|
|
|
|
down_output = rails("db:migrate:down:#{namespace}", "VERSION=#{version}")
|
|
|
|
up_output = rails("db:migrate:up:#{namespace}", "VERSION=#{version}")
|
|
|
|
else
|
|
|
|
assert_raises RuntimeError, /You're using a multiple database application/ do
|
|
|
|
down_output = rails("db:migrate:down", "VERSION=#{version}")
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_raises RuntimeError, /You're using a multiple database application/ do
|
|
|
|
up_output = rails("db:migrate:up", "VERSION=#{version}")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
case namespace
|
|
|
|
when "primary"
|
|
|
|
assert_match(/OneMigration: reverting/, down_output)
|
|
|
|
assert_match(/OneMigration: migrated/, up_output)
|
|
|
|
when nil
|
|
|
|
else
|
|
|
|
assert_match(/TwoMigration: reverting/, down_output)
|
|
|
|
assert_match(/TwoMigration: migrated/, up_output)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-02 10:11:09 -04:00
|
|
|
def db_prepare
|
|
|
|
Dir.chdir(app_path) do
|
|
|
|
generate_models_for_animals
|
|
|
|
output = rails("db:prepare")
|
|
|
|
|
|
|
|
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
|
|
|
|
if db_config.spec_name == "primary"
|
|
|
|
assert_match(/CreateBooks: migrated/, output)
|
|
|
|
else
|
|
|
|
assert_match(/CreateDogs: migrated/, output)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-04-06 14:23:59 -04:00
|
|
|
def write_models_for_animals
|
|
|
|
# make a directory for the animals migration
|
|
|
|
FileUtils.mkdir_p("#{app_path}/db/animals_migrate")
|
|
|
|
# move the dogs migration if it unless it already lives there
|
|
|
|
FileUtils.mv(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first, "db/animals_migrate/") unless Dir.glob("#{app_path}/db/animals_migrate/**/*dogs.rb").first
|
|
|
|
# delete the dogs migration if it's still present in the
|
|
|
|
# migrate folder. This is necessary because sometimes
|
|
|
|
# the code isn't fast enough and an extra migration gets made
|
|
|
|
FileUtils.rm(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first) if Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first
|
|
|
|
|
|
|
|
# change the base of the dog model
|
|
|
|
app_path("/app/models/dog.rb") do |file_name|
|
|
|
|
file = File.read("#{app_path}/app/models/dog.rb")
|
|
|
|
file.sub!(/ApplicationRecord/, "AnimalsBase")
|
|
|
|
File.write(file_name, file)
|
|
|
|
end
|
|
|
|
|
|
|
|
# create the base model for dog to inherit from
|
|
|
|
File.open("#{app_path}/app/models/animals_base.rb", "w") do |file|
|
2018-10-04 15:30:50 -04:00
|
|
|
file.write(<<~EOS)
|
|
|
|
class AnimalsBase < ActiveRecord::Base
|
|
|
|
self.abstract_class = true
|
2018-04-06 14:23:59 -04:00
|
|
|
|
2018-10-04 15:30:50 -04:00
|
|
|
establish_connection :animals
|
|
|
|
end
|
|
|
|
EOS
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-04 15:30:50 -04:00
|
|
|
def generate_models_for_animals
|
|
|
|
rails "generate", "model", "book", "title:string"
|
|
|
|
rails "generate", "model", "dog", "name:string"
|
|
|
|
write_models_for_animals
|
2019-02-18 14:06:28 -05:00
|
|
|
reload
|
2018-10-04 15:30:50 -04:00
|
|
|
end
|
|
|
|
|
2018-04-06 14:23:59 -04:00
|
|
|
test "db:create and db:drop works on all databases for env" do
|
|
|
|
require "#{app_path}/config/environment"
|
2018-08-31 10:55:37 -04:00
|
|
|
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
|
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.
2018-08-16 15:49:18 -04:00
|
|
|
db_create_and_drop db_config.spec_name, db_config.config["database"]
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "db:create:namespace and db:drop:namespace works on specified databases" do
|
|
|
|
require "#{app_path}/config/environment"
|
2018-08-31 10:55:37 -04:00
|
|
|
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
|
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.
2018-08-16 15:49:18 -04:00
|
|
|
db_create_and_drop_namespace db_config.spec_name, db_config.config["database"]
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "db:migrate and db:schema:dump and db:schema:load works on all databases" do
|
|
|
|
require "#{app_path}/config/environment"
|
2018-10-04 15:30:50 -04:00
|
|
|
db_migrate_and_schema_dump_and_load "schema"
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
test "db:migrate and db:structure:dump and db:structure:load works on all databases" do
|
2018-10-04 15:30:50 -04:00
|
|
|
require "#{app_path}/config/environment"
|
|
|
|
db_migrate_and_schema_dump_and_load "structure"
|
|
|
|
end
|
|
|
|
|
|
|
|
test "db:migrate:namespace works" do
|
2018-04-06 14:23:59 -04:00
|
|
|
require "#{app_path}/config/environment"
|
2018-08-31 10:55:37 -04:00
|
|
|
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
|
2018-11-08 09:03:44 -05:00
|
|
|
db_migrate_namespaced db_config.spec_name
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-08 15:44:51 -04:00
|
|
|
test "db:migrate:down and db:migrate:up without a namespace raises in a multi-db application" do
|
|
|
|
require "#{app_path}/config/environment"
|
|
|
|
|
|
|
|
app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
|
|
|
|
class OneMigration < ActiveRecord::Migration::Current
|
|
|
|
end
|
|
|
|
MIGRATION
|
|
|
|
|
|
|
|
db_up_and_down "01"
|
|
|
|
end
|
|
|
|
|
|
|
|
test "db:migrate:down:namespace and db:migrate:up:namespace works" do
|
|
|
|
require "#{app_path}/config/environment"
|
|
|
|
|
|
|
|
app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
|
|
|
|
class OneMigration < ActiveRecord::Migration::Current
|
|
|
|
end
|
|
|
|
MIGRATION
|
|
|
|
|
|
|
|
app_file "db/animals_migrate/02_two_migration.rb", <<-MIGRATION
|
|
|
|
class TwoMigration < ActiveRecord::Migration::Current
|
|
|
|
end
|
|
|
|
MIGRATION
|
|
|
|
|
|
|
|
db_up_and_down "01", "primary"
|
|
|
|
db_up_and_down "02", "animals"
|
|
|
|
end
|
|
|
|
|
2018-10-04 15:30:50 -04:00
|
|
|
test "db:migrate:status works on all databases" do
|
|
|
|
require "#{app_path}/config/environment"
|
|
|
|
db_migrate_and_migrate_status
|
|
|
|
end
|
|
|
|
|
|
|
|
test "db:migrate:status:namespace works" do
|
2018-04-06 14:23:59 -04:00
|
|
|
require "#{app_path}/config/environment"
|
2018-08-31 10:55:37 -04:00
|
|
|
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
|
2018-11-08 09:03:44 -05:00
|
|
|
db_migrate_namespaced db_config.spec_name
|
|
|
|
db_migrate_status_namespaced db_config.spec_name
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
end
|
2018-10-05 16:06:10 -04:00
|
|
|
|
|
|
|
test "db:schema:cache:dump works on all databases" do
|
|
|
|
require "#{app_path}/config/environment"
|
|
|
|
db_migrate_and_schema_cache_dump
|
|
|
|
end
|
|
|
|
|
|
|
|
test "db:schema:cache:clear works on all databases" do
|
|
|
|
require "#{app_path}/config/environment"
|
|
|
|
db_migrate_and_schema_cache_dump_and_schema_cache_clear
|
|
|
|
end
|
2019-04-02 10:11:09 -04:00
|
|
|
|
|
|
|
test "db:prepare works on all databases" do
|
|
|
|
require "#{app_path}/config/environment"
|
|
|
|
db_prepare
|
|
|
|
end
|
2018-04-06 14:23:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|