1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activerecord/test/cases/adapter_test.rb
eileencodes d9639a211f
Allow async executor to be configurable
This is a followup/alternative to #41406. This change wouldn't work for
GitHub because we intend to implement an executor for each database and
use the database configuration to set the `min_threads` and
`max_threads` for each one.

The changes here borrow from #41406 by implementing an
`Concurrent::ImmediateExecutor` by default. Otherwise applications have
the option of having one global thread pool that is used by all connections
or a thread pool for each connection. A global thread pool can set with
`config.active_record.async_query_executor = :global_thread_pool`. This
will create a single `Concurrent::ThreadPoolExecutor` for applications
to utilize. By default the concurrency is 4, but it can be changed for the
`global_thread_pool` by setting `global_executor_concurrency` to another
number. If applications want to use a thread pool per database
connection they can set `config.active_record.async_query_executor =
:multi_thread_pool`. This will create a `Concurrent::ThreadPoolExecutor`
for each database connection and set the `min_threads` and `max_threads`
by their configuration values or the defaults.

I've also moved the async tests out of the adapter test and into their
own tests and added tests for all the new functionality. This change
would allow us at GitHub to control threads per database and per
writer/reader or other apps to use one global executor. The immediate
executor allows apps to no-op by default.

Took the immediate executor idea from Jean's PR.
Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
2021-02-19 12:34:57 -05:00

565 lines
19 KiB
Ruby

# frozen_string_literal: true
require "cases/helper"
require "support/connection_helper"
require "models/book"
require "models/post"
require "models/author"
require "models/event"
module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
@connection.materialize_transactions
end
##
# PostgreSQL does not support null bytes in strings
unless current_adapter?(:PostgreSQLAdapter) ||
(current_adapter?(:SQLite3Adapter) && !ActiveRecord::Base.connection.prepared_statements)
def test_update_prepared_statement
b = Book.create(name: "my \x00 book")
b.reload
assert_equal "my \x00 book", b.name
b.update(name: "my other \x00 book")
b.reload
assert_equal "my other \x00 book", b.name
end
end
def test_create_record_with_pk_as_zero
Book.create(id: 0)
assert_equal 0, Book.find(0).id
assert_nothing_raised { Book.destroy(0) }
end
def test_valid_column
@connection.native_database_types.each_key do |type|
assert @connection.valid_type?(type)
end
end
def test_invalid_column
assert_not @connection.valid_type?(:foobar)
end
def test_tables
tables = @connection.tables
assert_includes tables, "accounts"
assert_includes tables, "authors"
assert_includes tables, "tasks"
assert_includes tables, "topics"
end
def test_table_exists?
assert @connection.table_exists?("accounts")
assert @connection.table_exists?(:accounts)
assert_not @connection.table_exists?("nonexistingtable")
assert_not @connection.table_exists?("'")
assert_not @connection.table_exists?(nil)
end
def test_data_sources
data_sources = @connection.data_sources
assert_includes data_sources, "accounts"
assert_includes data_sources, "authors"
assert_includes data_sources, "tasks"
assert_includes data_sources, "topics"
end
def test_data_source_exists?
assert @connection.data_source_exists?("accounts")
assert @connection.data_source_exists?(:accounts)
assert_not @connection.data_source_exists?("nonexistingtable")
assert_not @connection.data_source_exists?("'")
assert_not @connection.data_source_exists?(nil)
end
def test_indexes
idx_name = "accounts_idx"
indexes = @connection.indexes("accounts")
assert_empty indexes
@connection.add_index :accounts, :firm_id, name: idx_name
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
assert_equal idx_name, indexes.first.name
assert_not indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
ensure
@connection.remove_index(:accounts, name: idx_name) rescue nil
end
def test_remove_index_when_name_and_wrong_column_name_specified
index_name = "accounts_idx"
@connection.add_index :accounts, :firm_id, name: index_name
assert_raises ArgumentError do
@connection.remove_index :accounts, name: index_name, column: :wrong_column_name
end
ensure
@connection.remove_index(:accounts, name: index_name)
end
def test_remove_index_when_name_and_wrong_column_name_specified_positional_argument
index_name = "accounts_idx"
@connection.add_index :accounts, :firm_id, name: index_name
assert_raises ArgumentError do
@connection.remove_index :accounts, :wrong_column_name, name: index_name
end
ensure
@connection.remove_index(:accounts, name: index_name)
end
def test_current_database
if @connection.respond_to?(:current_database)
assert_equal ARTest.test_configuration_hashes["arunit"]["database"], @connection.current_database
end
end
def test_exec_query_returns_an_empty_result
result = @connection.exec_query "INSERT INTO subscribers(nick) VALUES('me')"
assert_instance_of(ActiveRecord::Result, result)
end
if current_adapter?(:Mysql2Adapter)
def test_charset
assert_not_nil @connection.charset
assert_not_equal "character_set_database", @connection.charset
assert_equal @connection.show_variable("character_set_database"), @connection.charset
end
def test_collation
assert_not_nil @connection.collation
assert_not_equal "collation_database", @connection.collation
assert_equal @connection.show_variable("collation_database"), @connection.collation
end
def test_show_nonexistent_variable_returns_nil
assert_nil @connection.show_variable("foo_bar_baz")
end
def test_not_specifying_database_name_for_cross_database_selects
assert_nothing_raised do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
ActiveRecord::Base.establish_connection(db_config.configuration_hash.except(:database))
config = ARTest.test_configuration_hashes
ActiveRecord::Base.connection.execute(
"SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
"FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
)
end
ensure
ActiveRecord::Base.establish_connection :arunit
end
end
def test_table_alias
def @connection.test_table_alias_length() 10; end
class << @connection
alias_method :old_table_alias_length, :table_alias_length
alias_method :table_alias_length, :test_table_alias_length
end
assert_equal "posts", @connection.table_alias_for("posts")
assert_equal "posts_comm", @connection.table_alias_for("posts_comments")
assert_equal "dbo_posts", @connection.table_alias_for("dbo.posts")
class << @connection
remove_method :table_alias_length
alias_method :table_alias_length, :old_table_alias_length
end
end
def test_uniqueness_violations_are_translated_to_specific_exception
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
error = assert_raises(ActiveRecord::RecordNotUnique) do
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
end
assert_not_nil error.cause
end
def test_not_null_violations_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::NotNullViolation) do
Post.create
end
assert_not_nil error.cause
end
unless current_adapter?(:SQLite3Adapter)
def test_value_limit_violations_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::ValueTooLong) do
Event.create(title: "abcdefgh")
end
assert_not_nil error.cause
end
def test_numeric_value_out_of_ranges_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::RangeError) do
Book.connection.create("INSERT INTO books(author_id) VALUES (9223372036854775808)")
end
assert_not_nil error.cause
end
end
def test_exceptions_from_notifications_are_not_translated
original_error = StandardError.new("This StandardError shouldn't get translated")
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") { raise original_error }
actual_error = assert_raises(StandardError) do
@connection.execute("SELECT * FROM posts")
end
assert_equal original_error, actual_error
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
def test_database_related_exceptions_are_translated_to_statement_invalid
error = assert_raises(ActiveRecord::StatementInvalid) do
@connection.execute("This is a syntax error")
end
assert_instance_of ActiveRecord::StatementInvalid, error
assert_kind_of Exception, error.cause
end
def test_select_all_always_return_activerecord_result
result = @connection.select_all "SELECT * FROM posts"
assert result.is_a?(ActiveRecord::Result)
end
if ActiveRecord::Base.connection.prepared_statements
def test_select_all_insert_update_delete_with_legacy_binds
binds = [[Event.column_for_attribute("id"), 1]]
bind_param = Arel::Nodes::BindParam.new(nil)
assert_deprecated do
id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds)
assert_equal 1, id
end
assert_deprecated do
updated = @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, updated
end
assert_deprecated do
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal({ "id" => 1, "title" => "foo" }, result.first)
end
assert_deprecated do
deleted = @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, deleted
end
assert_deprecated do
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_nil result.first
end
end
def test_select_all_insert_update_delete_with_casted_binds
binds = [Event.type_for_attribute("id").serialize(1)]
bind_param = Arel::Nodes::BindParam.new(nil)
id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds)
assert_equal 1, id
updated = @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, updated
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal({ "id" => 1, "title" => "foo" }, result.first)
deleted = @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, deleted
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_nil result.first
end
def test_select_all_insert_update_delete_with_binds
binds = [Relation::QueryAttribute.new("id", 1, Event.type_for_attribute("id"))]
bind_param = Arel::Nodes::BindParam.new(nil)
id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds)
assert_equal 1, id
updated = @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, updated
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal({ "id" => 1, "title" => "foo" }, result.first)
deleted = @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, deleted
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_nil result.first
end
end
def test_select_methods_passing_a_association_relation
author = Author.create!(name: "john")
Post.create!(author: author, title: "foo", body: "bar")
query = author.posts.where(title: "foo").select(:title)
assert_equal({ "title" => "foo" }, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
assert_equal ["foo"], @connection.select_values(query)
end
def test_select_methods_passing_a_relation
Post.create!(title: "foo", body: "bar")
query = Post.where(title: "foo").select(:title)
assert_equal({ "title" => "foo" }, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
assert_equal ["foo"], @connection.select_values(query)
end
test "type_to_sql returns a String for unmapped types" do
assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
end
def test_allowed_index_name_length_is_deprecated
assert_deprecated { @connection.allowed_index_name_length }
end
unless current_adapter?(:OracleAdapter)
def test_in_clause_length_is_deprecated
assert_deprecated { @connection.in_clause_length }
end
end
end
class AdapterForeignKeyTest < ActiveRecord::TestCase
self.use_transactional_tests = false
fixtures :fk_test_has_pk
def setup
@connection = ActiveRecord::Base.connection
end
def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
klass_has_fk = Class.new(ActiveRecord::Base) do
self.table_name = "fk_test_has_fk"
end
error = assert_raises(ActiveRecord::InvalidForeignKey) do
has_fk = klass_has_fk.new
has_fk.fk_id = 1231231231
has_fk.save(validate: false)
end
assert_not_nil error.cause
end
def test_foreign_key_violations_on_insert_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::InvalidForeignKey) do
insert_into_fk_test_has_fk
end
assert_not_nil error.cause
end
def test_foreign_key_violations_on_delete_are_translated_to_specific_exception
insert_into_fk_test_has_fk fk_id: 1
error = assert_raises(ActiveRecord::InvalidForeignKey) do
@connection.execute "DELETE FROM fk_test_has_pk WHERE pk_id = 1"
end
assert_not_nil error.cause
end
def test_disable_referential_integrity
assert_nothing_raised do
@connection.disable_referential_integrity do
insert_into_fk_test_has_fk
# should delete created record as otherwise disable_referential_integrity will try to enable constraints
# after executed block and will fail (at least on Oracle)
@connection.execute "DELETE FROM fk_test_has_fk"
end
end
end
private
def insert_into_fk_test_has_fk(fk_id: 0)
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
@connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},#{fk_id})"
else
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (#{fk_id})"
end
end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_tests = false
fixtures :posts, :authors, :author_addresses
def setup
@connection = ActiveRecord::Base.connection
end
unless in_memory_db?
test "reconnect after a disconnect" do
assert_predicate @connection, :active?
@connection.disconnect!
assert_not_predicate @connection, :active?
@connection.reconnect!
assert_predicate @connection, :active?
end
test "transaction state is reset after a reconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
@connection.reconnect!
assert_not_predicate @connection, :transaction_open?
end
test "transaction state is reset after a disconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
@connection.disconnect!
assert_not_predicate @connection, :transaction_open?
ensure
@connection.reconnect!
end
end
def test_create_with_query_cache
@connection.enable_query_cache!
count = Post.count
@connection.create("INSERT INTO posts(title, body) VALUES ('', '')")
assert_equal count + 1, Post.count
ensure
reset_fixtures("posts")
@connection.disable_query_cache!
end
def test_truncate
assert_operator Post.count, :>, 0
@connection.truncate("posts")
assert_equal 0, Post.count
ensure
reset_fixtures("posts")
end
def test_truncate_with_query_cache
@connection.enable_query_cache!
assert_operator Post.count, :>, 0
@connection.truncate("posts")
assert_equal 0, Post.count
ensure
reset_fixtures("posts")
@connection.disable_query_cache!
end
def test_truncate_tables
assert_operator Post.count, :>, 0
assert_operator Author.count, :>, 0
assert_operator AuthorAddress.count, :>, 0
@connection.truncate_tables("author_addresses", "authors", "posts")
assert_equal 0, Post.count
assert_equal 0, Author.count
assert_equal 0, AuthorAddress.count
ensure
reset_fixtures("posts", "authors", "author_addresses")
end
def test_truncate_tables_with_query_cache
@connection.enable_query_cache!
assert_operator Post.count, :>, 0
assert_operator Author.count, :>, 0
assert_operator AuthorAddress.count, :>, 0
@connection.truncate_tables("author_addresses", "authors", "posts")
assert_equal 0, Post.count
assert_equal 0, Author.count
assert_equal 0, AuthorAddress.count
ensure
reset_fixtures("posts", "authors", "author_addresses")
@connection.disable_query_cache!
end
# test resetting sequences in odd tables in PostgreSQL
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
require "models/movie"
require "models/subscriber"
def test_reset_empty_table_with_custom_pk
Movie.delete_all
Movie.connection.reset_pk_sequence! "movies"
assert_equal 1, Movie.create(name: "fight club").id
end
def test_reset_table_with_non_integer_pk
Subscriber.delete_all
Subscriber.connection.reset_pk_sequence! "subscribers"
sub = Subscriber.new(name: "robert drake")
sub.id = "bob drake"
assert_nothing_raised { sub.save! }
end
end
private
def reset_fixtures(*fixture_names)
ActiveRecord::FixtureSet.reset_cache
fixture_names.each do |fixture_name|
ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, fixture_name)
end
end
end
end
if ActiveRecord::Base.connection.supports_advisory_locks?
class AdvisoryLocksEnabledTest < ActiveRecord::TestCase
include ConnectionHelper
def test_advisory_locks_enabled?
assert ActiveRecord::Base.connection.advisory_locks_enabled?
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(
orig_connection.merge(advisory_locks: false)
)
assert_not ActiveRecord::Base.connection.advisory_locks_enabled?
ActiveRecord::Base.establish_connection(
orig_connection.merge(advisory_locks: true)
)
assert ActiveRecord::Base.connection.advisory_locks_enabled?
end
end
end
end