Eliminate missed `lease_connection` calls

Followup: https://github.com/rails/rails/pull/51353
Fix: https://github.com/rails/rails/issues/51722

Not too sure what happened, but I somehow missed quite a number
of `lease_connection` calls inside Active Record.
This commit is contained in:
Jean Boussier 2024-05-03 12:53:22 +02:00
parent 793ff00442
commit fa048105d1
18 changed files with 289 additions and 230 deletions

View File

@ -6,21 +6,23 @@ module ActiveRecord
module Associations
# Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
def self.create(connection, initial_table, joins, aliases = nil)
if joins.empty?
aliases ||= Hash.new(0)
elsif aliases
default_proc = aliases.default_proc || proc { 0 }
aliases.default_proc = proc { |h, k|
h[k] = initial_count_for(connection, k, joins) + default_proc.call(h, k)
}
else
aliases = Hash.new { |h, k|
h[k] = initial_count_for(connection, k, joins)
}
def self.create(pool, initial_table, joins, aliases = nil)
pool.with_connection do |connection|
if joins.empty?
aliases ||= Hash.new(0)
elsif aliases
default_proc = aliases.default_proc || proc { 0 }
aliases.default_proc = proc { |h, k|
h[k] = initial_count_for(connection, k, joins) + default_proc.call(h, k)
}
else
aliases = Hash.new { |h, k|
h[k] = initial_count_for(connection, k, joins)
}
end
aliases[initial_table] = 1
new(connection.table_alias_length, aliases)
end
aliases[initial_table] = 1
new(connection, aliases)
end
def self.initial_count_for(connection, name, table_joins)
@ -46,9 +48,9 @@ module ActiveRecord
end
# table_joins is an array of arel joins which might conflict with the aliases we assign here
def initialize(connection, aliases)
@aliases = aliases
@connection = connection
def initialize(table_alias_length, aliases)
@aliases = aliases
@table_alias_length = table_alias_length
end
def aliased_table_for(arel_table, table_name = nil)
@ -60,7 +62,7 @@ module ActiveRecord
arel_table = arel_table.alias(table_name) if arel_table.name != table_name
else
# Otherwise, we need to use an alias
aliased_name = @connection.table_alias_for(yield)
aliased_name = table_alias_for(yield)
# Update the count
count = aliases[aliased_name] += 1
@ -76,8 +78,12 @@ module ActiveRecord
attr_reader :aliases
private
def table_alias_for(table_name)
table_name[0...@table_alias_length].tr(".", "_")
end
def truncate(name)
name.slice(0, @connection.table_alias_length - 2)
name.slice(0, @table_alias_length - 2)
end
end
end

View File

@ -239,8 +239,10 @@ module ActiveRecord
def _default_attributes # :nodoc:
@default_attributes ||= begin
attributes_hash = columns_hash.transform_values do |column|
ActiveModel::Attribute.from_database(column.name, column.default, type_for_column(column))
attributes_hash = with_connection do |connection|
columns_hash.transform_values do |column|
ActiveModel::Attribute.from_database(column.name, column.default, type_for_column(connection, column))
end
end
attribute_set = ActiveModel::AttributeSet.new(attributes_hash)
@ -295,7 +297,7 @@ module ActiveRecord
Type.lookup(name, **options, adapter: Type.adapter_name_from(self))
end
def type_for_column(column)
def type_for_column(connection, column)
hook_attribute_type(column.name, super)
end
end

View File

@ -93,8 +93,8 @@ module ActiveRecord
end
end
def lease_connection(**)
connection = super
def checkout_and_verify(connection)
super
connection.query_cache ||= query_cache
connection
end
@ -147,6 +147,12 @@ module ActiveRecord
end
end
def query_cache
@thread_query_caches.compute_if_absent(ActiveSupport::IsolatedExecutionState.context) do
Store.new(@query_cache_max_size)
end
end
private
def prune_thread_cache
dead_threads = @thread_query_caches.keys.reject(&:alive?)
@ -154,12 +160,6 @@ module ActiveRecord
@thread_query_caches.delete(dead_thread)
end
end
def query_cache
@thread_query_caches.compute_if_absent(ActiveSupport::IsolatedExecutionState.context) do
Store.new(@query_cache_max_size)
end
end
end
attr_accessor :query_cache

View File

@ -376,9 +376,9 @@ module ActiveRecord
TypeCaster::Map.new(self)
end
def cached_find_by_statement(key, &block) # :nodoc:
cache = @find_by_statement_cache[lease_connection.prepared_statements]
cache.compute_if_absent(key) { StatementCache.create(lease_connection, &block) }
def cached_find_by_statement(connection, key, &block) # :nodoc:
cache = @find_by_statement_cache[connection.prepared_statements]
cache.compute_if_absent(key) { StatementCache.create(connection, &block) }
end
private
@ -419,21 +419,23 @@ module ActiveRecord
end
def cached_find_by(keys, values)
statement = cached_find_by_statement(keys) { |params|
wheres = keys.index_with do |key|
if key.is_a?(Array)
[key.map { params.bind }]
else
params.bind
with_connection do |connection|
statement = cached_find_by_statement(connection, keys) { |params|
wheres = keys.index_with do |key|
if key.is_a?(Array)
[key.map { params.bind }]
else
params.bind
end
end
end
where(wheres).limit(1)
}
where(wheres).limit(1)
}
begin
statement.execute(values.flatten, lease_connection, allow_retry: true).first
rescue TypeError
raise ActiveRecord::StatementInvalid
begin
statement.execute(values.flatten, lease_connection, allow_retry: true).first
rescue TypeError
raise ActiveRecord::StatementInvalid
end
end
end
end

View File

@ -379,7 +379,7 @@ module ActiveRecord
def reset_sequence_name # :nodoc:
@explicit_sequence_name = false
@sequence_name = lease_connection.default_sequence_name(table_name, primary_key)
@sequence_name = with_connection { |c| c.default_sequence_name(table_name, primary_key) }
end
# Sets the name of the sequence to use when generating ids to the given
@ -404,13 +404,13 @@ module ActiveRecord
# Determines if the primary key values should be selected from their
# corresponding sequence before the insert statement.
def prefetch_primary_key?
lease_connection.prefetch_primary_key?(table_name)
with_connection { |c| c.prefetch_primary_key?(table_name) }
end
# Returns the next value that will be used as the primary key on
# an insert statement.
def next_sequence_value
lease_connection.next_sequence_value(sequence_name)
with_connection { |c| c.next_sequence_value(sequence_name) }
end
# Indicates whether the table associated with this class exists
@ -435,10 +435,10 @@ module ActiveRecord
@columns ||= columns_hash.values.freeze
end
def _returning_columns_for_insert # :nodoc:
def _returning_columns_for_insert(connection) # :nodoc:
@_returning_columns_for_insert ||= begin
auto_populated_columns = columns.filter_map do |c|
c.name if lease_connection.return_value_after_insert?(c)
c.name if connection.return_value_after_insert?(c)
end
auto_populated_columns.empty? ? Array(primary_key) : auto_populated_columns
@ -523,7 +523,7 @@ module ActiveRecord
# end
# end
def reset_column_information
lease_connection.clear_cache!
connection_pool.active_connection&.clear_cache!
([self] + descendants).each(&:undefine_attribute_methods)
schema_cache.clear_data_source_cache!(table_name)
@ -617,8 +617,8 @@ module ActiveRecord
end
end
def type_for_column(column)
type = lease_connection.lookup_cast_type_from_column(column)
def type_for_column(connection, column)
type = connection.lookup_cast_type_from_column(column)
if immutable_strings_by_default && type.respond_to?(:to_immutable_string)
type = type.to_immutable_string

View File

@ -568,7 +568,7 @@ module ActiveRecord
delete_by(primary_key => id_or_array)
end
def _insert_record(values, returning) # :nodoc:
def _insert_record(connection, values, returning) # :nodoc:
primary_key = self.primary_key
primary_key_value = nil
@ -583,12 +583,12 @@ module ActiveRecord
with_connection do |c|
if values.empty?
im.insert(c.empty_insert_statement_value(primary_key))
im.insert(connection.empty_insert_statement_value(primary_key))
else
im.insert(values.transform_keys { |name| arel_table[name] })
end
c.insert(
connection.insert(
im, "#{self} Create", primary_key || false, primary_key_value,
returning: returning
)
@ -1255,16 +1255,19 @@ module ActiveRecord
def _create_record(attribute_names = self.attribute_names)
attribute_names = attributes_for_create(attribute_names)
returning_columns = self.class._returning_columns_for_insert
self.class.with_connection do |connection|
returning_columns = self.class._returning_columns_for_insert(connection)
returning_values = self.class._insert_record(
attributes_with_values(attribute_names),
returning_columns
)
returning_values = self.class._insert_record(
connection,
attributes_with_values(attribute_names),
returning_columns
)
returning_columns.zip(returning_values).each do |column, value|
_write_attribute(column, value) if !_read_attribute(column)
end if returning_values
returning_columns.zip(returning_values).each do |column, value|
_write_attribute(column, value) if !_read_attribute(column)
end if returning_values
end
@new_record = false
@previously_new_record = true

View File

@ -48,18 +48,25 @@ module ActiveRecord
# Note that building your own SQL query string from user input may expose your application to
# injection attacks (https://guides.rubyonrails.org/security.html#sql-injection).
def find_by_sql(sql, binds = [], preparable: nil, allow_retry: false, &block)
_load_from_sql(_query_by_sql(sql, binds, preparable: preparable, allow_retry: allow_retry), &block)
result = with_connection do |c|
_query_by_sql(c, sql, binds, preparable: preparable, allow_retry: allow_retry)
end
_load_from_sql(result, &block)
end
# Same as <tt>#find_by_sql</tt> but perform the query asynchronously and returns an ActiveRecord::Promise.
def async_find_by_sql(sql, binds = [], preparable: nil, &block)
_query_by_sql(sql, binds, preparable: preparable, async: true).then do |result|
result = with_connection do |c|
_query_by_sql(c, sql, binds, preparable: preparable, async: true)
end
result.then do |result|
_load_from_sql(result, &block)
end
end
def _query_by_sql(sql, binds = [], preparable: nil, async: false, allow_retry: false) # :nodoc:
lease_connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable, async: async, allow_retry: allow_retry)
def _query_by_sql(connection, sql, binds = [], preparable: nil, async: false, allow_retry: false) # :nodoc:
connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable, async: async, allow_retry: allow_retry)
end
def _load_from_sql(result_set, &block) # :nodoc:
@ -99,12 +106,16 @@ module ActiveRecord
#
# * +sql+ - An SQL statement which should return a count query from the database, see the example above.
def count_by_sql(sql)
lease_connection.select_value(sanitize_sql(sql), "#{name} Count").to_i
with_connection do |c|
c.select_value(sanitize_sql(sql), "#{name} Count").to_i
end
end
# Same as <tt>#count_by_sql</tt> but perform the query asynchronously and returns an ActiveRecord::Promise.
def async_count_by_sql(sql)
lease_connection.select_value(sanitize_sql(sql), "#{name} Count", async: true).then(&:to_i)
with_connection do |c|
c.select_value(sanitize_sql(sql), "#{name} Count", async: true).then(&:to_i)
end
end
end
end

View File

@ -529,7 +529,9 @@ module ActiveRecord
if polymorphic?
key = [key, owner._read_attribute(@foreign_type)]
end
klass.cached_find_by_statement(key, &block)
klass.with_connection do |connection|
klass.cached_find_by_statement(connection, key, &block)
end
end
def join_table

View File

@ -263,12 +263,14 @@ module ActiveRecord
# and failed due to validation errors it won't be persisted, you get what #create returns in
# such situation.
def create_or_find_by(attributes, &block)
transaction(requires_new: true) { create(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
if lease_connection.transaction_open?
where(attributes).lock.find_by!(attributes)
else
find_by!(attributes)
with_connection do |connection|
transaction(requires_new: true) { create(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
if connection.transaction_open?
where(attributes).lock.find_by!(attributes)
else
find_by!(attributes)
end
end
end
@ -276,12 +278,14 @@ module ActiveRecord
# {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
# is raised if the created record is invalid.
def create_or_find_by!(attributes, &block)
transaction(requires_new: true) { create!(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
if lease_connection.transaction_open?
where(attributes).lock.find_by!(attributes)
else
find_by!(attributes)
with_connection do |connection|
transaction(requires_new: true) { create!(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
if connection.transaction_open?
where(attributes).lock.find_by!(attributes)
else
find_by!(attributes)
end
end
end
@ -590,18 +594,18 @@ module ActiveRecord
values = Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
end
arel = eager_loading? ? apply_join_dependency.arel : build_arel
arel.source.left = table
group_values_arel_columns = arel_columns(group_values.uniq)
having_clause_ast = having_clause.ast unless having_clause.empty?
key = if klass.composite_primary_key?
primary_key.map { |pk| table[pk] }
else
table[primary_key]
end
stmt = arel.compile_update(values, key, having_clause_ast, group_values_arel_columns)
klass.with_connection do |c|
arel = eager_loading? ? apply_join_dependency.arel : build_arel(c)
arel.source.left = table
group_values_arel_columns = arel_columns(group_values.uniq)
having_clause_ast = having_clause.ast unless having_clause.empty?
key = if klass.composite_primary_key?
primary_key.map { |pk| table[pk] }
else
table[primary_key]
end
stmt = arel.compile_update(values, key, having_clause_ast, group_values_arel_columns)
c.update(stmt, "#{klass} Update All").tap { reset }
end
end
@ -730,19 +734,19 @@ module ActiveRecord
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
end
arel = eager_loading? ? apply_join_dependency.arel : build_arel
arel.source.left = table
group_values_arel_columns = arel_columns(group_values.uniq)
having_clause_ast = having_clause.ast unless having_clause.empty?
key = if klass.composite_primary_key?
primary_key.map { |pk| table[pk] }
else
table[primary_key]
end
stmt = arel.compile_delete(key, having_clause_ast, group_values_arel_columns)
klass.with_connection do |c|
arel = eager_loading? ? apply_join_dependency.arel : build_arel(c)
arel.source.left = table
group_values_arel_columns = arel_columns(group_values.uniq)
having_clause_ast = having_clause.ast unless having_clause.empty?
key = if klass.composite_primary_key?
primary_key.map { |pk| table[pk] }
else
table[primary_key]
end
stmt = arel.compile_delete(key, having_clause_ast, group_values_arel_columns)
c.delete(stmt, "#{klass} Delete All").tap { reset }
end
end
@ -947,7 +951,7 @@ module ActiveRecord
end
def alias_tracker(joins = [], aliases = nil) # :nodoc:
ActiveRecord::Associations::AliasTracker.create(lease_connection, table.name, joins, aliases)
ActiveRecord::Associations::AliasTracker.create(connection_pool, table.name, joins, aliases)
end
class StrictLoadingScope # :nodoc:
@ -1075,7 +1079,7 @@ module ActiveRecord
if where_clause.contradiction?
[].freeze
elsif eager_loading?
with_connection do |c|
klass.with_connection do |c|
apply_join_dependency do |relation, join_dependency|
if relation.null_relation?
[].freeze
@ -1087,7 +1091,9 @@ module ActiveRecord
end
end
else
klass._query_by_sql(arel, async: async)
klass.with_connection do |c|
klass._query_by_sql(c, arel, async: async)
end
end
end
end

View File

@ -510,73 +510,73 @@ module ActiveRecord
end
group_fields = arel_columns(group_fields)
column_alias_tracker = ColumnAliasTracker.new(lease_connection)
@klass.with_connection do |connection|
column_alias_tracker = ColumnAliasTracker.new(connection)
group_aliases = group_fields.map { |field|
field = lease_connection.visitor.compile(field) if Arel.arel_node?(field)
column_alias_tracker.alias_for(field.to_s.downcase)
}
group_columns = group_aliases.zip(group_fields)
group_aliases = group_fields.map { |field|
field = connection.visitor.compile(field) if Arel.arel_node?(field)
column_alias_tracker.alias_for(field.to_s.downcase)
}
group_columns = group_aliases.zip(group_fields)
column = aggregate_column(column_name)
column_alias = column_alias_tracker.alias_for("#{operation} #{column_name.to_s.downcase}")
select_value = operation_over_aggregate_column(column, operation, distinct)
select_value.as(adapter_class.quote_column_name(column_alias))
column = aggregate_column(column_name)
column_alias = column_alias_tracker.alias_for("#{operation} #{column_name.to_s.downcase}")
select_value = operation_over_aggregate_column(column, operation, distinct)
select_value.as(adapter_class.quote_column_name(column_alias))
select_values = [select_value]
select_values += self.select_values unless having_clause.empty?
select_values = [select_value]
select_values += self.select_values unless having_clause.empty?
select_values.concat group_columns.map { |aliaz, field|
aliaz = adapter_class.quote_column_name(aliaz)
if field.respond_to?(:as)
field.as(aliaz)
else
"#{field} AS #{aliaz}"
end
}
relation = except(:group).distinct!(false)
relation.group_values = group_fields
relation.select_values = select_values
result = skip_query_cache_if_necessary do
@klass.with_connection do |c|
c.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: @async)
end
end
result.then do |calculated_data|
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
key_records = key_records.index_by(&:id)
end
key_types = group_columns.each_with_object({}) do |(aliaz, col_name), types|
types[aliaz] = col_name.try(:type_caster) ||
type_for(col_name) do
calculated_data.column_types.fetch(aliaz, Type.default_value)
end
end
hash_rows = calculated_data.cast_values(key_types).map! do |row|
calculated_data.columns.each_with_object({}).with_index do |(col_name, hash), i|
hash[col_name] = row[i]
select_values.concat group_columns.map { |aliaz, field|
aliaz = adapter_class.quote_column_name(aliaz)
if field.respond_to?(:as)
field.as(aliaz)
else
"#{field} AS #{aliaz}"
end
}
relation = except(:group).distinct!(false)
relation.group_values = group_fields
relation.select_values = select_values
result = skip_query_cache_if_necessary do
connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: @async)
end
if operation != "count"
type = column.try(:type_caster) ||
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
type = type.subtype if Enum::EnumType === type
end
result.then do |calculated_data|
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
key_records = key_records.index_by(&:id)
end
hash_rows.each_with_object({}) do |row, result|
key = group_aliases.map { |aliaz| row[aliaz] }
key = key.first if key.size == 1
key = key_records[key] if associated
key_types = group_columns.each_with_object({}) do |(aliaz, col_name), types|
types[aliaz] = col_name.try(:type_caster) ||
type_for(col_name) do
calculated_data.column_types.fetch(aliaz, Type.default_value)
end
end
result[key] = type_cast_calculated_value(row[column_alias], operation, type)
hash_rows = calculated_data.cast_values(key_types).map! do |row|
calculated_data.columns.each_with_object({}).with_index do |(col_name, hash), i|
hash[col_name] = row[i]
end
end
if operation != "count"
type = column.try(:type_caster) ||
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
type = type.subtype if Enum::EnumType === type
end
hash_rows.each_with_object({}) do |row, result|
key = group_aliases.map { |aliaz| row[aliaz] }
key = key.first if key.size == 1
key = key_records[key] if associated
result[key] = type_cast_calculated_value(row[column_alias], operation, type)
end
end
end
end

View File

@ -1570,7 +1570,7 @@ module ActiveRecord
# Returns the Arel object associated with the relation.
def arel(aliases = nil) # :nodoc:
@arel ||= build_arel(aliases)
@arel ||= with_connection { |c| build_arel(c, aliases) }
end
def construct_join_dependency(associations, join_type) # :nodoc:
@ -1709,14 +1709,14 @@ module ActiveRecord
raise ImmutableRelation if @loaded || @arel
end
def build_arel(aliases = nil)
def build_arel(connection, aliases = nil)
arel = Arel::SelectManager.new(table)
build_joins(arel.join_sources, aliases)
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
arel.take(build_cast_value("LIMIT", lease_connection.sanitize_limit(limit_value))) if limit_value
arel.take(build_cast_value("LIMIT", connection.sanitize_limit(limit_value))) if limit_value
arel.skip(build_cast_value("OFFSET", offset_value.to_i)) if offset_value
arel.group(*arel_columns(group_values.uniq)) unless group_values.empty?

View File

@ -163,13 +163,19 @@ module ActiveRecord
def sanitize_sql_array(ary)
statement, *values = ary
if values.first.is_a?(Hash) && /:\w+/.match?(statement)
replace_named_bind_variables(statement, values.first)
with_connection do |c|
replace_named_bind_variables(c, statement, values.first)
end
elsif statement.include?("?")
replace_bind_variables(statement, values)
with_connection do |c|
replace_bind_variables(c, statement, values)
end
elsif statement.blank?
statement
else
statement % values.collect { |value| lease_connection.quote_string(value.to_s) }
with_connection do |c|
statement % values.collect { |value| c.quote_string(value.to_s) }
end
end
end
@ -193,48 +199,47 @@ module ActiveRecord
end
private
def replace_bind_variables(statement, values)
def replace_bind_variables(connection, statement, values)
raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)
bound = values.dup
c = lease_connection
statement.gsub(/\?/) do
replace_bind_variable(bound.shift, c)
replace_bind_variable(connection, bound.shift)
end
end
def replace_bind_variable(value, c = lease_connection)
def replace_bind_variable(connection, value)
if ActiveRecord::Relation === value
value.to_sql
else
quote_bound_value(value, c)
quote_bound_value(connection, value)
end
end
def replace_named_bind_variables(statement, bind_vars)
def replace_named_bind_variables(connection, statement, bind_vars)
statement.gsub(/([:\\]?):([a-zA-Z]\w*)/) do |match|
if $1 == ":" # skip PostgreSQL casts
match # return the whole match
elsif $1 == "\\" # escaped literal colon
match[1..-1] # return match with escaping backlash char removed
elsif bind_vars.include?(match = $2.to_sym)
replace_bind_variable(bind_vars[match])
replace_bind_variable(connection, bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
end
end
end
def quote_bound_value(value, c = lease_connection)
def quote_bound_value(connection, value)
if value.respond_to?(:map) && !value.acts_like?(:string)
values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
if values.empty?
c.quote(c.cast_bound_value(nil))
connection.quote(connection.cast_bound_value(nil))
else
values.map! { |v| c.quote(c.cast_bound_value(v)) }.join(",")
values.map! { |v| connection.quote(connection.cast_bound_value(v)) }.join(",")
end
else
value = value.id_for_database if value.respond_to?(:id_for_database)
c.quote(c.cast_bound_value(value))
connection.quote(connection.cast_bound_value(value))
end
end

View File

@ -75,7 +75,7 @@ module ActiveRecord
end
def current_time_from_proper_timezone
lease_connection.default_timezone == :utc ? Time.now.utc : Time.now
with_connection { |c| c.default_timezone == :utc ? Time.now.utc : Time.now }
end
protected

View File

@ -226,7 +226,7 @@ module ActiveRecord
# Returns the current transaction. See ActiveRecord::Transactions API docs.
def current_transaction
connection_pool.active_connection&.current_transaction || ConnectionAdapters::NULL_TRANSACTION
connection_pool.active_connection&.current_transaction || ConnectionAdapters::TransactionManager::NULL_TRANSACTION
end
def before_commit(*args, &block) # :nodoc:

View File

@ -258,7 +258,7 @@ if ActiveRecord::Base.lease_connection.prepared_statements
end
def cached_statement(klass, key)
cache = klass.send(:cached_find_by_statement, key) do
cache = klass.send(:cached_find_by_statement, @connection, key) do
raise "#{klass} has no cached statement by #{key.inspect}"
end
cache.send(:query_builder).instance_variable_get(:@sql)

View File

@ -1,9 +1,12 @@
# frozen_string_literal: true
require "cases/helper"
require "models/post"
module ActiveRecord
class ConnectionHandlingTest < ActiveRecord::TestCase
fixtures :posts
setup do
@_permanent_connection_checkout_was = ActiveRecord.permanent_connection_checkout
end
@ -164,6 +167,21 @@ module ActiveRecord
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
end
test "common APIs don't permanently hold a connection when permanent checkout is deprecated or disallowed" do
ActiveRecord.permanent_connection_checkout = :deprecated
ActiveRecord::Base.release_connection
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
Post.create!(title: "foo", body: "bar")
assert_not_predicate Post.connection_pool, :active_connection?
Post.first
assert_not_predicate Post.connection_pool, :active_connection?
Post.count
assert_not_predicate Post.connection_pool, :active_connection?
end
end
end
end

View File

@ -35,8 +35,8 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def teardown
Task.lease_connection.clear_query_cache
ActiveRecord::Base.lease_connection.disable_query_cache!
Task.connection_pool.clear_query_cache
ActiveRecord::Base.connection_pool.disable_query_cache!
super
end
@ -45,10 +45,10 @@ class QueryCacheTest < ActiveRecord::TestCase
mw = middleware { |env|
Post.first
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 1, query_cache.size, query_cache.inspect
Post.lease_connection.execute("SELECT 1")
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 0, query_cache.size, query_cache.inspect
}
mw.call({})
@ -61,10 +61,10 @@ class QueryCacheTest < ActiveRecord::TestCase
mw = middleware { |env|
Post.first
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 1, query_cache.size, query_cache.inspect
Post.lease_connection.exec_query("SELECT 1")
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 0, query_cache.size, query_cache.inspect
}
mw.call({})
@ -77,13 +77,13 @@ class QueryCacheTest < ActiveRecord::TestCase
mw = middleware { |env|
Post.first
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 1, query_cache.size, query_cache.inspect
Post.lease_connection.uncached do
# should clear the cache
Post.create!(title: "a new post", body: "and a body")
end
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 0, query_cache.size, query_cache.inspect
}
mw.call({})
@ -96,12 +96,12 @@ class QueryCacheTest < ActiveRecord::TestCase
mw = middleware { |env|
Post.first
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 1, query_cache.size, query_cache.inspect
Post.lease_connection.uncached do
Post.count # shouldn't clear the cache
end
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 1, query_cache.size, query_cache.inspect
}
mw.call({})
@ -115,7 +115,7 @@ class QueryCacheTest < ActiveRecord::TestCase
mw = middleware { |env|
Task.find 1
Task.find 1
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 1, query_cache.size, query_cache.inspect
raise "lol borked"
}
@ -287,7 +287,7 @@ class QueryCacheTest < ActiveRecord::TestCase
mw = middleware { |env|
Task.find 1
Task.find 1
query_cache = ActiveRecord::Base.lease_connection.query_cache
query_cache = ActiveRecord::Base.connection_pool.query_cache
assert_equal 1, query_cache.size, query_cache.inspect
[200, {}, nil]
}
@ -597,11 +597,11 @@ class QueryCacheTest < ActiveRecord::TestCase
middleware {
assert ActiveRecord::Base.connection_pool.query_cache_enabled
assert ActiveRecord::Base.lease_connection.query_cache_enabled
assert ActiveRecord::Base.connection_pool.query_cache_enabled
Thread.new {
assert_not ActiveRecord::Base.connection_pool.query_cache_enabled
assert_not ActiveRecord::Base.lease_connection.query_cache_enabled
assert_not ActiveRecord::Base.connection_pool.query_cache_enabled
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
}.join
@ -612,7 +612,7 @@ class QueryCacheTest < ActiveRecord::TestCase
middleware {
ActiveRecord::Base.connection_handler.connection_pool_list(:all).each do |pool|
assert pool.query_cache_enabled
assert pool.lease_connection.query_cache_enabled
assert pool.with_connection(&:query_cache_enabled)
end
}.call({})
end
@ -709,11 +709,11 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_query_cache_uncached_dirties
mw = middleware { |env|
Post.first
assert_no_changes -> { ActiveRecord::Base.lease_connection.query_cache.size } do
assert_no_changes -> { ActiveRecord::Base.connection_pool.query_cache.size } do
Post.uncached(dirties: false) { Post.create!(title: "a new post", body: "and a body") }
end
assert_changes -> { ActiveRecord::Base.lease_connection.query_cache.size }, from: 1, to: 0 do
assert_changes -> { ActiveRecord::Base.connection_pool.query_cache.size }, from: 1, to: 0 do
Post.uncached(dirties: true) { Post.create!(title: "a new post", body: "and a body") }
end
}
@ -723,11 +723,11 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_query_cache_connection_uncached_dirties
mw = middleware { |env|
Post.first
assert_no_changes -> { ActiveRecord::Base.lease_connection.query_cache.size } do
assert_no_changes -> { ActiveRecord::Base.connection_pool.query_cache.size } do
Post.lease_connection.uncached(dirties: false) { Post.create!(title: "a new post", body: "and a body") }
end
assert_changes -> { ActiveRecord::Base.lease_connection.query_cache.size }, from: 1, to: 0 do
assert_changes -> { ActiveRecord::Base.connection_pool.query_cache.size }, from: 1, to: 0 do
Post.lease_connection.uncached(dirties: true) { Post.create!(title: "a new post", body: "and a body") }
end
}
@ -737,7 +737,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_query_cache_uncached_dirties_disabled_with_nested_cache
mw = middleware { |env|
Post.first
assert_changes -> { ActiveRecord::Base.lease_connection.query_cache.size }, from: 1, to: 0 do
assert_changes -> { ActiveRecord::Base.connection_pool.query_cache.size }, from: 1, to: 0 do
Post.uncached(dirties: false) do
Post.cache do
Post.create!(title: "a new post", body: "and a body")
@ -746,7 +746,7 @@ class QueryCacheTest < ActiveRecord::TestCase
end
Post.first
assert_changes -> { ActiveRecord::Base.lease_connection.query_cache.size }, from: 1, to: 0 do
assert_changes -> { ActiveRecord::Base.connection_pool.query_cache.size }, from: 1, to: 0 do
Post.lease_connection.uncached(dirties: false) do
Post.lease_connection.cache do
Post.create!(title: "a new post", body: "and a body")
@ -911,36 +911,36 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
end
def test_find
assert_called(Task.lease_connection.query_cache, :clear, times: 1) do
assert_not Task.lease_connection.query_cache_enabled
assert_called(Task.connection_pool.query_cache, :clear, times: 1) do
assert_not Task.connection_pool.query_cache_enabled
Task.cache do
assert Task.lease_connection.query_cache_enabled
assert Task.connection_pool.query_cache_enabled
Task.find(1)
Task.uncached do
assert_not Task.lease_connection.query_cache_enabled
assert_not Task.connection_pool.query_cache_enabled
Task.find(1)
end
assert Task.lease_connection.query_cache_enabled
assert Task.connection_pool.query_cache_enabled
end
assert_not Task.lease_connection.query_cache_enabled
assert_not Task.connection_pool.query_cache_enabled
end
end
def test_enable_disable
assert_called(Task.lease_connection.query_cache, :clear, times: 1) do
assert_called(Task.connection_pool.query_cache, :clear, times: 1) do
Task.cache { }
end
assert_called(Task.lease_connection.query_cache, :clear, times: 1) do
assert_called(Task.connection_pool.query_cache, :clear, times: 1) do
Task.cache { Task.cache { } }
end
end
def test_update
Task.cache do
assert_called(Task.lease_connection.query_cache, :clear, times: 1) do
assert_called(Task.connection_pool.query_cache, :clear, times: 1) do
task = Task.find(1)
task.starting = Time.now.utc
task.save!
@ -950,7 +950,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_destroy
Task.cache do
assert_called(Task.lease_connection.query_cache, :clear, times: 1) do
assert_called(Task.connection_pool.query_cache, :clear, times: 1) do
Task.find(1).destroy
end
end
@ -958,7 +958,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_insert
Task.cache do
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
Task.create!
end
end
@ -968,11 +968,11 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
skip unless supports_insert_on_duplicate_skip?
Task.cache do
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
Task.insert({ starting: Time.now })
end
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
Task.insert_all([{ starting: Time.now }])
end
end
@ -980,11 +980,11 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_insert_all_bang
Task.cache do
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
Task.insert!({ starting: Time.now })
end
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
Task.insert_all!([{ starting: Time.now }])
end
end
@ -994,11 +994,11 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
skip unless supports_insert_on_duplicate_update?
Task.cache do
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
Task.upsert({ starting: Time.now })
end
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
Task.upsert_all([{ starting: Time.now }])
end
end
@ -1006,7 +1006,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_cache_is_expired_by_habtm_update
ActiveRecord::Base.cache do
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
c = Category.first
p = Post.first
p.categories << c
@ -1016,7 +1016,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_cache_is_expired_by_habtm_delete
ActiveRecord::Base.cache do
assert_called(ActiveRecord::Base.lease_connection.query_cache, :clear, times: 1) do
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
p = Post.find(1)
assert_predicate p.categories, :any?
p.categories.delete_all

View File

@ -242,9 +242,13 @@ class SanitizeTest < ActiveRecord::TestCase
private
def bind(statement, *vars)
if vars.first.is_a?(Hash)
ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first)
ActiveRecord::Base.with_connection do |c|
ActiveRecord::Base.send(:replace_named_bind_variables, c, statement, vars.first)
end
else
ActiveRecord::Base.send(:replace_bind_variables, statement, vars)
ActiveRecord::Base.with_connection do |c|
ActiveRecord::Base.send(:replace_bind_variables, c, statement, vars)
end
end
end
end