Allow `truncate` for SQLite3 adapter and add `rails db:seed:replant` (#34779)

* Add `ActiveRecord::Base.connection.truncate` for SQLite3 adapter.

SQLite doesn't support `TRUNCATE TABLE`, but SQLite3 adapter can support
`ActiveRecord::Base.connection.truncate` by using `DELETE FROM`.

`DELETE` without `WHERE` uses "The Truncate Optimization",
see https://www.sqlite.org/lang_delete.html.

* Add `rails db:seed:replant` that truncates database tables and loads the seeds

Closes #34765
This commit is contained in:
Bogdan 2019-03-05 00:57:38 +02:00 committed by David Heinemeier Hansson
parent 076e8eddf4
commit a8c0ebccbd
10 changed files with 334 additions and 1 deletions

View File

@ -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. * Deprecate mismatched collation comparison for uniqueness validator.
Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1. Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.

View File

@ -154,6 +154,10 @@ module ActiveRecord
@statements.clear @statements.clear
end end
def truncate(table_name, name = nil)
execute "DELETE FROM #{quote_table_name(table_name)}", name
end
def supports_index_sort_order? def supports_index_sort_order?
true true
end end

View File

@ -66,6 +66,11 @@ db_namespace = namespace :db do
end end
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." # 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 task purge: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.purge_current ActiveRecord::Tasks::DatabaseTasks.purge_current
@ -223,6 +228,11 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.load_seed ActiveRecord::Tasks::DatabaseTasks.load_seed
end 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 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." 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 task load: :load_config do

View File

@ -182,6 +182,24 @@ module ActiveRecord
} }
end 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 def migrate
check_target_version check_target_version

View File

@ -31,6 +31,16 @@ module ActiveRecord
connection.recreate_database configuration["database"], creation_options connection.recreate_database configuration["database"], creation_options
end 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 def charset
connection.charset connection.charset
end end

View File

@ -48,6 +48,18 @@ module ActiveRecord
create true create true
end 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) def structure_dump(filename, extra_flags)
set_psql_env set_psql_env

View File

@ -33,6 +33,16 @@ module ActiveRecord
create create
end 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 def charset
connection.encoding connection.encoding
end end

View File

@ -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

View File

@ -2,6 +2,7 @@
require "cases/helper" require "cases/helper"
require "active_record/tasks/database_tasks" require "active_record/tasks/database_tasks"
require "models/author"
module ActiveRecord module ActiveRecord
module DatabaseTasksSetupper module DatabaseTasksSetupper
@ -944,6 +945,127 @@ module ActiveRecord
end end
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 class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper include DatabaseTasksSetupper

View File

@ -1,11 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
require "isolation/abstract_unit" require "isolation/abstract_unit"
require "env_helpers"
module ApplicationTests module ApplicationTests
module RakeTests module RakeTests
class RakeDbsTest < ActiveSupport::TestCase class RakeDbsTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation include ActiveSupport::Testing::Isolation, EnvHelpers
def setup def setup
build_app build_app
@ -139,6 +140,59 @@ module ApplicationTests
end end
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) def db_migrate_and_status(expected_database)
rails "generate", "model", "book", "title:string" rails "generate", "model", "book", "title:string"
rails "db:migrate" rails "db:migrate"
@ -387,6 +441,72 @@ module ApplicationTests
assert_equal "test", test_environment.call assert_equal "test", test_environment.call
end 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 end
end end