diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 979b48c654..04449a3ebc 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,12 @@ +* Add `rails db:seed:replant` that truncates tables of each database + for current environment and loads the seeds. + + *bogdanvlviv*, *DHH* + +* Add `ActiveRecord::Base.connection.truncate` for SQLite3 adapter. + + *bogdanvlviv* + * Deprecate mismatched collation comparison for uniqueness validator. Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 9f28fdf749..63e07932d9 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -154,6 +154,10 @@ module ActiveRecord @statements.clear end + def truncate(table_name, name = nil) + execute "DELETE FROM #{quote_table_name(table_name)}", name + end + def supports_index_sort_order? true end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 8de06e8466..f021a8f6c4 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -66,6 +66,11 @@ db_namespace = namespace :db do end end + # desc "Truncates tables of each database for current environment" + task truncate_all: [:load_config, :check_protected_environments] do + ActiveRecord::Tasks::DatabaseTasks.truncate_all + end + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." task purge: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.purge_current @@ -223,6 +228,11 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.load_seed end + namespace :seed do + desc "Truncates tables of each database for current environment and loads the seeds" + task replant: [:load_config, :truncate_all, :seed] + end + namespace :fixtures do desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task load: :load_config do diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 27e401a756..ef02ca8190 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -182,6 +182,24 @@ module ActiveRecord } end + def truncate_tables(configuration) + ActiveRecord::Base.connected_to(database: { truncation: configuration }) do + table_names = ActiveRecord::Base.connection.tables + internal_table_names = [ + ActiveRecord::Base.schema_migrations_table_name, + ActiveRecord::Base.internal_metadata_table_name + ] + + class_for_adapter(configuration["adapter"]).new(configuration).truncate_tables(*table_names.without(*internal_table_names)) + end + end + + def truncate_all(environment = env) + ActiveRecord::Base.configurations.configs_for(env_name: environment).each do |db_config| + truncate_tables db_config.config + end + end + def migrate check_target_version diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 1c1b29b5e1..f2b4ead98d 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -31,6 +31,16 @@ module ActiveRecord connection.recreate_database configuration["database"], creation_options end + def truncate_tables(*table_names) + return if table_names.empty? + + ActiveRecord::Base.connection.disable_referential_integrity do + table_names.each do |table_name| + ActiveRecord::Base.connection.truncate(table_name) + end + end + end + def charset connection.charset end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 8acb11f75f..dc368eb97d 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -48,6 +48,18 @@ module ActiveRecord create true end + def truncate_tables(*table_names) + return if table_names.empty? + + ActiveRecord::Base.connection.disable_referential_integrity do + quoted_table_names = table_names.map do |table_name| + ActiveRecord::Base.connection.quote_table_name(table_name) + end + + ActiveRecord::Base.connection.execute "TRUNCATE TABLE #{quoted_table_names.join(", ")}" + end + end + def structure_dump(filename, extra_flags) set_psql_env diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index a82cea80ca..cedbae6b7f 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -33,6 +33,16 @@ module ActiveRecord create end + def truncate_tables(*table_names) + return if table_names.empty? + + ActiveRecord::Base.connection.disable_referential_integrity do + table_names.each do |table_name| + ActiveRecord::Base.connection.truncate(table_name) + end + end + end + def charset connection.encoding end diff --git a/activerecord/test/cases/adapters/sqlite3/connection_test.rb b/activerecord/test/cases/adapters/sqlite3/connection_test.rb new file mode 100644 index 0000000000..3dabc8766a --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/connection_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "cases/helper" + +class SQLite3ConnectionTest < ActiveRecord::SQLite3TestCase + fixtures :comments + + def test_truncate + rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") + count = rows.first.values.first + assert_operator count, :>, 0 + + ActiveRecord::Base.connection.truncate("comments") + rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") + count = rows.first.values.first + assert_equal 0, count + end +end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 3fd1813d64..bd6fb099a4 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "active_record/tasks/database_tasks" +require "models/author" module ActiveRecord module DatabaseTasksSetupper @@ -944,6 +945,127 @@ module ActiveRecord end end + unless in_memory_db? + class DatabaseTasksTruncateAllTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :authors, :author_addresses + + def test_truncate_tables + assert_operator Author.count, :>, 0 + assert_operator AuthorAddress.count, :>, 0 + + old_configurations = ActiveRecord::Base.configurations + configurations = { development: ActiveRecord::Base.configurations["arunit"] } + ActiveRecord::Base.configurations = configurations + + ActiveRecord::Tasks::DatabaseTasks.stub(:root, nil) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("development") + ) + end + + assert_equal 0, Author.count + assert_equal 0, AuthorAddress.count + ensure + ActiveRecord::Base.configurations = old_configurations + end + end + end + + class DatabaseTasksTruncateAllWithMultipleDatabasesTest < 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" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } } + } + end + + def test_truncate_all_databases_for_environment + with_stubbed_configurations do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :truncate_tables, + [ + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end + + def test_truncate_all_databases_with_url_for_environment + with_stubbed_configurations do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :truncate_tables, + [ + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], + ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("production") + ) + end + end + end + + def test_truncate_all_development_databases_when_env_was_no_specified + with_stubbed_configurations do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :truncate_tables, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + + def test_truncate_all_development_databases_when_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + + with_stubbed_configurations do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :truncate_tables, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("development") + ) + end + end + ensure + ENV["RAILS_ENV"] = old_env + end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end + end + class DatabaseTasksCharsetTest < ActiveRecord::TestCase include DatabaseTasksSetupper diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index ba5704c53e..a8ec1f3663 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true require "isolation/abstract_unit" +require "env_helpers" module ApplicationTests module RakeTests class RakeDbsTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation + include ActiveSupport::Testing::Isolation, EnvHelpers def setup build_app @@ -139,6 +140,59 @@ module ApplicationTests end end + test "db:truncate_all truncates all not internal tables" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "db:migrate" + require "#{app_path}/config/environment" + Book.create!(title: "Remote") + assert_equal 1, Book.count + schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + + rails "db:truncate_all" + + assert_equal( + schema_migrations, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + ) + assert_equal( + internal_metadata, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + ) + assert_equal 0, Book.count + end + end + + test "db:truncate_all does not truncate any tables when environment is protected" do + with_rails_env "production" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "db:migrate" + require "#{app_path}/config/environment" + Book.create!(title: "Remote") + assert_equal 1, Book.count + schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + books = ActiveRecord::Base.connection.execute("SELECT * from \"books\"") + + output = rails("db:truncate_all", allow_failure: true) + assert_match(/ActiveRecord::ProtectedEnvironmentError/, output) + + assert_equal( + schema_migrations, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + ) + assert_equal( + internal_metadata, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + ) + assert_equal 1, Book.count + assert_equal(books, ActiveRecord::Base.connection.execute("SELECT * from \"books\"")) + end + end + end + def db_migrate_and_status(expected_database) rails "generate", "model", "book", "title:string" rails "db:migrate" @@ -387,6 +441,72 @@ module ApplicationTests assert_equal "test", test_environment.call end + + test "db:seed:replant truncates all not internal tables and loads the seeds" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "db:migrate" + require "#{app_path}/config/environment" + Book.create!(title: "Remote") + assert_equal 1, Book.count + schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + + app_file "db/seeds.rb", <<-RUBY + Book.create!(title: "Rework") + Book.create!(title: "Ruby Under a Microscope") + RUBY + + rails "db:seed:replant" + + assert_equal( + schema_migrations, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + ) + assert_equal( + internal_metadata, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + ) + assert_equal 2, Book.count + assert_not_predicate Book.where(title: "Remote"), :exists? + assert_predicate Book.where(title: "Rework"), :exists? + assert_predicate Book.where(title: "Ruby Under a Microscope"), :exists? + end + end + + test "db:seed:replant does not truncate any tables and does not load the seeds when environment is protected" do + with_rails_env "production" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "db:migrate" + require "#{app_path}/config/environment" + Book.create!(title: "Remote") + assert_equal 1, Book.count + schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + books = ActiveRecord::Base.connection.execute("SELECT * from \"books\"") + + app_file "db/seeds.rb", <<-RUBY + Book.create!(title: "Rework") + RUBY + + output = rails("db:seed:replant", allow_failure: true) + assert_match(/ActiveRecord::ProtectedEnvironmentError/, output) + + assert_equal( + schema_migrations, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + ) + assert_equal( + internal_metadata, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + ) + assert_equal 1, Book.count + assert_equal(books, ActiveRecord::Base.connection.execute("SELECT * from \"books\"")) + assert_not_predicate Book.where(title: "Rework"), :exists? + end + end + end end end end