Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Active Record adapters connections lazy #42251

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ def requires_reloading?
# This is done under the hood by calling #active?. If the connection
# is no longer active, then this method will reconnect to the database.
def verify!
return if @connection.nil?
reconnect! unless active?
end

Expand All @@ -569,7 +570,7 @@ def verify!
# PostgreSQL's lo_* methods.
def raw_connection
disable_lazy_transactions!
@connection
connection
end

def default_uniqueness_comparison(attribute, value) # :nodoc:
Expand Down Expand Up @@ -628,6 +629,13 @@ def check_version # :nodoc:
end

private
def connection
@connection || @lock.synchronize do
connect
@connection
end
end

def type_map
@type_map ||= Type::TypeMap.new.tap do |mapping|
initialize_type_map(mapping)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def execute(sql, name = nil, async: false)

# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone

super
end
Expand Down Expand Up @@ -92,7 +92,7 @@ def default_insert_value(column)
end

def last_inserted_id(result)
@connection.last_id
@connection&.last_id
end

def multi_statements_enabled?
Expand All @@ -109,13 +109,13 @@ def with_multi_statements
multi_statements_was = multi_statements_enabled?

unless multi_statements_was
@connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON)
connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON)
end

yield
ensure
unless multi_statements_was
@connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF)
connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ def mysql2_connection(config)
config[:flags] |= Mysql2::Client::FOUND_ROWS
end

ConnectionAdapters::Mysql2Adapter.new(
ConnectionAdapters::Mysql2Adapter.new_client(config),
logger,
nil,
config,
)
ConnectionAdapters::Mysql2Adapter.new(nil, logger, nil, config)
end
end

Expand All @@ -50,11 +45,10 @@ def new_client(config)
def initialize(connection, logger, connection_options, config)
superclass_config = config.reverse_merge(prepared_statements: false)
super(connection, logger, connection_options, superclass_config)
configure_connection
end

def self.database_exists?(config)
!!ActiveRecord::Base.mysql2_connection(config)
!!ActiveRecord::Base.mysql2_connection(config).raw_connection
rescue ActiveRecord::NoDatabaseError
false
end
Expand Down Expand Up @@ -100,7 +94,7 @@ def error_number(exception)
#++

def quote_string(string)
@connection.escape(string)
connection.escape(string)
rescue Mysql2::Error => error
raise translate_exception(error, message: error.message, sql: "<escape>", binds: [])
end
Expand All @@ -110,7 +104,7 @@ def quote_string(string)
#++

def active?
@connection.ping
@connection&.ping
end

def reconnect!
Expand All @@ -124,19 +118,25 @@ def reconnect!
# Otherwise, this method does nothing.
def disconnect!
super
@connection.close
@connection&.close
end

def discard! # :nodoc:
super
@connection.automatic_close = false
@connection&.automatic_close = false
@connection = nil
end

private
def connect
@connection = self.class.new_client(@config)
configure_connection
rescue Mysql2::Error => error
if error.error_number == ER_BAD_DB_ERROR
raise ActiveRecord::NoDatabaseError
else
raise
end
end

def configure_connection
Expand All @@ -149,7 +149,7 @@ def full_version
end

def get_full_version
@connection.server_info[:version]
connection.server_info[:version]
end

def translate_exception(exception, message:, sql:, binds:)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def execute(sql, name = nil) #:nodoc:

log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@connection.execute(sql)
connection.execute(sql)
end
end
end
Expand All @@ -43,7 +43,7 @@ def exec_query(sql, name = nil, binds = [], prepare: false, async: false)
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
# Don't cache statements if they are not prepared
unless prepare
stmt = @connection.prepare(sql)
stmt = connection.prepare(sql)
begin
cols = stmt.columns
unless without_prepared_statement?(binds)
Expand All @@ -54,7 +54,7 @@ def exec_query(sql, name = nil, binds = [], prepare: false, async: false)
stmt.close
end
else
stmt = @statements[sql] ||= @connection.prepare(sql)
stmt = @statements[sql] ||= connection.prepare(sql)
cols = stmt.columns
stmt.reset!
stmt.bind_params(type_casted_binds)
Expand All @@ -68,30 +68,30 @@ def exec_query(sql, name = nil, binds = [], prepare: false, async: false)

def exec_delete(sql, name = "SQL", binds = [])
exec_query(sql, name, binds)
@connection.changes
connection.changes
end
alias :exec_update :exec_delete

def begin_isolated_db_transaction(isolation) #:nodoc
raise TransactionIsolationError, "SQLite3 only supports the `read_uncommitted` transaction isolation level" if isolation != :read_uncommitted
raise StandardError, "You need to enable the shared-cache mode in SQLite mode before attempting to change the transaction isolation level" unless shared_cache?

Thread.current.thread_variable_set("read_uncommitted", @connection.get_first_value("PRAGMA read_uncommitted"))
@connection.read_uncommitted = true
Thread.current.thread_variable_set("read_uncommitted", connection.get_first_value("PRAGMA read_uncommitted"))
connection.read_uncommitted = true
begin_db_transaction
end

def begin_db_transaction #:nodoc:
log("begin transaction", "TRANSACTION") { @connection.transaction }
log("begin transaction", "TRANSACTION") { connection.transaction }
end

def commit_db_transaction #:nodoc:
log("commit transaction", "TRANSACTION") { @connection.commit }
log("commit transaction", "TRANSACTION") { connection.commit }
reset_read_uncommitted
end

def exec_rollback_db_transaction #:nodoc:
log("rollback transaction", "TRANSACTION") { @connection.rollback }
log("rollback transaction", "TRANSACTION") { connection.rollback }
reset_read_uncommitted
end

Expand All @@ -100,7 +100,7 @@ def reset_read_uncommitted
read_uncommitted = Thread.current.thread_variable_get("read_uncommitted")
return unless read_uncommitted

@connection.read_uncommitted = read_uncommitted
connection.read_uncommitted = read_uncommitted
end

def execute_batch(statements, name = nil)
Expand All @@ -113,13 +113,13 @@ def execute_batch(statements, name = nil)

log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@connection.execute_batch2(sql)
connection.execute_batch2(sql)
end
end
end

def last_inserted_id(result)
@connection.last_insert_row_id
connection.last_insert_row_id
end

def build_fixture_statements(fixture_set)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module ConnectionAdapters
module SQLite3
module Quoting # :nodoc:
def quote_string(s)
@connection.class.quote(s)
::SQLite3::Database.quote(s)
end

def quote_table_name_for_assignment(table, attr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,7 @@ def sqlite3_connection(config)
Dir.mkdir(dirname) unless File.directory?(dirname)
end

db = SQLite3::Database.new(
config[:database].to_s,
config.merge(results_as_hash: true)
)

ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
ConnectionAdapters::SQLite3Adapter.new(nil, logger, nil, config)
rescue Errno::ENOENT => error
if error.message.include?("No such file or directory")
raise ActiveRecord::NoDatabaseError
Expand Down Expand Up @@ -86,7 +81,6 @@ def dealloc(stmt)
def initialize(connection, logger, connection_options, config)
@memory_database = config[:database] == ":memory:"
super(connection, logger, config)
configure_connection
end

def self.database_exists?(config)
Expand Down Expand Up @@ -159,19 +153,19 @@ def supports_concurrent_connections?
end

def active?
!@connection.closed?
!@connection.nil? && !@connection.closed?
end

def reconnect!
super
connect if @connection.closed?
connect if @connection&.closed?
end

# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
super
@connection.close rescue nil
@connection&.close rescue nil
end

def supports_index_sort_order?
Expand All @@ -184,7 +178,7 @@ def native_database_types #:nodoc:

# Returns the current database encoding format as a string, eg: 'UTF-8'
def encoding
@connection.encoding.to_s
connection.encoding.to_s
end

def supports_explain?
Expand Down Expand Up @@ -555,6 +549,12 @@ def connect
@config.merge(results_as_hash: true)
)
configure_connection
rescue Errno::ENOENT => error
if error.message.include?("No such file or directory")
raise ActiveRecord::NoDatabaseError
else
raise
end
end

def configure_connection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def setup

def test_connection_error
assert_raises ActiveRecord::ConnectionNotEstablished do
ActiveRecord::Base.mysql2_connection(socket: File::NULL)
ActiveRecord::Base.mysql2_connection(socket: File::NULL).execute("SELECT 1")
end
end

Expand Down
28 changes: 17 additions & 11 deletions activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,12 @@ def test_connection_no_db

def test_bad_timeout
assert_raises(TypeError) do
Base.sqlite3_connection database: ":memory:",
adapter: "sqlite3",
timeout: "usa"
adapter = Base.sqlite3_connection(
database: ":memory:",
adapter: "sqlite3",
timeout: "usa",
)
adapter.raw_connection
end
end

Expand Down Expand Up @@ -279,6 +282,7 @@ def test_tables
end

def test_tables_logs_name
@conn.raw_connection
sql = <<~SQL
SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table')
SQL
Expand Down Expand Up @@ -568,14 +572,16 @@ def test_statement_closed
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
db = ::SQLite3::Database.new(db_config.database)

statement = ::SQLite3::Statement.new(db,
"CREATE TABLE statement_test (number integer not null)")
statement.stub(:step, -> { raise ::SQLite3::BusyException.new("busy") }) do
assert_called(statement, :columns, returns: []) do
assert_called(statement, :close) do
::SQLite3::Statement.stub(:new, statement) do
assert_raises ActiveRecord::StatementInvalid do
@conn.exec_query "select * from statement_test"
@conn.stub(:connection, db) do
statement = ::SQLite3::Statement.new(db,
"CREATE TABLE statement_test (number integer not null)")
statement.stub(:step, -> { raise ::SQLite3::BusyException.new("busy") }) do
assert_called(statement, :columns, returns: []) do
assert_called(statement, :close) do
::SQLite3::Statement.stub(:new, statement) do
assert_raises ActiveRecord::StatementInvalid do
@conn.exec_query "select * from statement_test"
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def test_sqlite_creates_directory
@conn = Base.sqlite3_connection database: dir.join("db/foo.sqlite3"),
adapter: "sqlite3",
timeout: 100
@conn.raw_connection

assert Dir.exist? dir.join("db")
assert File.exist? dir.join("db/foo.sqlite3")
Expand Down