Skip to content

Commit 407031f

Browse files
authored
Merge pull request rails#50396 from Shopify/stricter-relation-delegation
Make the Relation -> Model delegation stricter
2 parents f6918a5 + fd5bd98 commit 407031f

File tree

13 files changed

+56
-50
lines changed

13 files changed

+56
-50
lines changed

activerecord/lib/active_record/associations/preloader/association.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ def initialize(scope, association_key_name)
1717
def eql?(other)
1818
association_key_name == other.association_key_name &&
1919
scope.table_name == other.scope.table_name &&
20-
scope.connection_specification_name == other.scope.connection_specification_name &&
20+
scope.model.connection_specification_name == other.scope.model.connection_specification_name &&
2121
scope.values_for_queries == other.scope.values_for_queries
2222
end
2323

2424
def hash
25-
[association_key_name, scope.table_name, scope.connection_specification_name, scope.values_for_queries].hash
25+
[association_key_name, scope.model.table_name, scope.model.connection_specification_name, scope.values_for_queries].hash
2626
end
2727

2828
def records_for(loaders)

activerecord/lib/active_record/encryption/extended_deterministic_queries.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def self.install_support
4141
module EncryptedQuery # :nodoc:
4242
class << self
4343
def process_arguments(owner, args, check_for_additional_values)
44+
owner = owner.model if owner.is_a?(Relation)
45+
4446
return args if owner.deterministic_encrypted_attributes&.empty?
4547

4648
if args.is_a?(Array) && (options = args.first).is_a?(Hash)

activerecord/lib/active_record/relation.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ def compute_cache_key(timestamp_column = :updated_at) # :nodoc:
437437
query_signature = ActiveSupport::Digest.hexdigest(to_sql)
438438
key = "#{klass.model_name.cache_key}/query-#{query_signature}"
439439

440-
if collection_cache_versioning
440+
if model.collection_cache_versioning
441441
key
442442
else
443443
"#{key}-#{compute_cache_version(timestamp_column)}"
@@ -456,7 +456,7 @@ def compute_cache_key(timestamp_column = :updated_at) # :nodoc:
456456
#
457457
# SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
458458
def cache_version(timestamp_column = :updated_at)
459-
if collection_cache_versioning
459+
if model.collection_cache_versioning
460460
@cache_versions ||= {}
461461
@cache_versions[timestamp_column] ||= compute_cache_version(timestamp_column)
462462
end
@@ -475,7 +475,7 @@ def compute_cache_version(timestamp_column) # :nodoc:
475475

476476
with_connection do |c|
477477
column = c.visitor.compile(table[timestamp_column])
478-
select_values = "COUNT(*) AS #{adapter_class.quote_column_name("size")}, MAX(%s) AS timestamp"
478+
select_values = "COUNT(*) AS #{klass.adapter_class.quote_column_name("size")}, MAX(%s) AS timestamp"
479479

480480
if collection.has_limit_or_offset?
481481
query = collection.select("#{column} AS collection_cache_key_timestamp")
@@ -501,7 +501,7 @@ def compute_cache_version(timestamp_column) # :nodoc:
501501
end
502502

503503
if timestamp
504-
"#{size}-#{timestamp.utc.to_fs(cache_timestamp_format)}"
504+
"#{size}-#{timestamp.utc.to_fs(model.cache_timestamp_format)}"
505505
else
506506
"#{size}"
507507
end
@@ -1291,7 +1291,7 @@ def has_limit_or_offset? # :nodoc:
12911291
end
12921292

12931293
def alias_tracker(joins = [], aliases = nil) # :nodoc:
1294-
ActiveRecord::Associations::AliasTracker.create(connection_pool, table.name, joins, aliases)
1294+
ActiveRecord::Associations::AliasTracker.create(klass.connection_pool, table.name, joins, aliases)
12951295
end
12961296

12971297
class StrictLoadingScope # :nodoc:
@@ -1451,7 +1451,7 @@ def instantiate_records(rows, &block)
14511451

14521452
def skip_query_cache_if_necessary(&block)
14531453
if skip_query_cache_value
1454-
uncached(&block)
1454+
model.uncached(&block)
14551455
else
14561456
yield
14571457
end

activerecord/lib/active_record/relation/batches.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ def act_on_ignored_order(error_on_ignore)
327327

328328
if raise_error
329329
raise ArgumentError.new(ORDER_IGNORE_MESSAGE)
330-
elsif logger
331-
logger.warn(ORDER_IGNORE_MESSAGE)
330+
elsif model.logger
331+
model.logger.warn(ORDER_IGNORE_MESSAGE)
332332
end
333333
end
334334

activerecord/lib/active_record/relation/calculations.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,13 +522,13 @@ def execute_grouped_calculation(operation, column_name, distinct) # :nodoc:
522522
column = aggregate_column(column_name)
523523
column_alias = column_alias_tracker.alias_for("#{operation} #{column_name.to_s.downcase}")
524524
select_value = operation_over_aggregate_column(column, operation, distinct)
525-
select_value.as(adapter_class.quote_column_name(column_alias))
525+
select_value.as(klass.adapter_class.quote_column_name(column_alias))
526526

527527
select_values = [select_value]
528528
select_values += self.select_values unless having_clause.empty?
529529

530530
select_values.concat group_columns.map { |aliaz, field|
531-
aliaz = adapter_class.quote_column_name(aliaz)
531+
aliaz = klass.adapter_class.quote_column_name(aliaz)
532532
if field.respond_to?(:as)
533533
field.as(aliaz)
534534
else
@@ -633,6 +633,7 @@ def select_for_count
633633
if select_values.present?
634634
return select_values.first if select_values.one?
635635

636+
adapter_class = klass.adapter_class
636637
select_values.map do |field|
637638
column = arel_column(field.to_s) do |attr_name|
638639
Arel.sql(attr_name)

activerecord/lib/active_record/relation/delegation.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def uncacheable_methods
2222
end
2323

2424
module DelegateCache # :nodoc:
25+
@delegate_base_methods = true
26+
singleton_class.attr_accessor :delegate_base_methods
27+
2528
def relation_delegate_class(klass)
2629
@relation_delegate_cache[klass]
2730
end
@@ -100,7 +103,7 @@ def #{method}(...)
100103
:to_sentence, :to_fs, :to_formatted_s, :as_json,
101104
:shuffle, :split, :slice, :index, :rindex, to: :records
102105

103-
delegate :primary_key, :lease_connection, :connection, :with_connection, :transaction, to: :klass
106+
delegate :primary_key, :with_connection, :connection, :table_name, :transaction, :sanitize_sql_like, :unscoped, to: :klass
104107

105108
module ClassSpecificRelation # :nodoc:
106109
extend ActiveSupport::Concern
@@ -114,9 +117,17 @@ def name
114117
private
115118
def method_missing(method, ...)
116119
if @klass.respond_to?(method)
117-
unless Delegation.uncacheable_methods.include?(method)
120+
if !DelegateCache.delegate_base_methods && Base.respond_to?(method)
121+
# A common mistake in Active Record's own code is to call `ActiveRecord::Base`
122+
# class methods on Association. It works because it's automatically delegated, but
123+
# can introduce subtle bugs because it sets the global scope.
124+
# We can't deprecate this behavior because gems might depend on it, however we
125+
# can ban it from Active Record's own test suite to avoid regressions.
126+
raise NotImplementedError, "Active Record code shouldn't rely on association delegation into ActiveRecord::Base methods"
127+
elsif !Delegation.uncacheable_methods.include?(method)
118128
@klass.generate_relation_method(method)
119129
end
130+
120131
scoping { @klass.public_send(method, ...) }
121132
else
122133
super

activerecord/lib/active_record/relation/finder_methods.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@ def sole
145145

146146
if found.nil?
147147
raise_record_not_found_exception!
148-
elsif undesired.present?
149-
raise ActiveRecord::SoleRecordExceeded.new(self)
150-
else
148+
elsif undesired.nil?
151149
found
150+
else
151+
raise ActiveRecord::SoleRecordExceeded.new(model)
152152
end
153153
end
154154

@@ -376,7 +376,7 @@ def exists?(conditions = :none)
376376

377377
skip_query_cache_if_necessary do
378378
with_connection do |c|
379-
c.select_rows(relation.arel, "#{name} Exists?").size == 1
379+
c.select_rows(relation.arel, "#{klass.name} Exists?").size == 1
380380
end
381381
end
382382
end
@@ -638,7 +638,7 @@ def find_last(limit)
638638
end
639639

640640
def ordered_relation
641-
if order_values.empty? && (implicit_order_column || !query_constraints_list.nil? || primary_key)
641+
if order_values.empty? && (model.implicit_order_column || !model.query_constraints_list.nil? || primary_key)
642642
order(_order_columns.map { |column| table[column].asc })
643643
else
644644
self
@@ -648,11 +648,11 @@ def ordered_relation
648648
def _order_columns
649649
oc = []
650650

651-
oc << implicit_order_column if implicit_order_column
652-
oc << query_constraints_list if query_constraints_list
651+
oc << model.implicit_order_column if model.implicit_order_column
652+
oc << model.query_constraints_list if model.query_constraints_list
653653

654-
if primary_key && query_constraints_list.nil?
655-
oc << primary_key
654+
if model.primary_key && model.query_constraints_list.nil?
655+
oc << model.primary_key
656656
end
657657

658658
oc.flatten.uniq.compact

activerecord/lib/active_record/relation/query_methods.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,10 @@ def missing(*associations)
136136

137137
private
138138
def scope_association_reflection(association)
139-
reflection = @scope.klass._reflect_on_association(association)
139+
model = @scope.model
140+
reflection = model._reflect_on_association(association)
140141
unless reflection
141-
raise ArgumentError.new("An association named `:#{association}` does not exist on the model `#{@scope.name}`.")
142+
raise ArgumentError.new("An association named `:#{association}` does not exist on the model `#{model.name}`.")
142143
end
143144
reflection
144145
end
@@ -254,6 +255,10 @@ def includes!(*args) # :nodoc:
254255
self
255256
end
256257

258+
def all # :nodoc:
259+
spawn
260+
end
261+
257262
# Specify associations +args+ to be eager loaded using a <tt>LEFT OUTER JOIN</tt>.
258263
# Performs a single query joining all specified associations. For example:
259264
#
@@ -703,7 +708,7 @@ def in_order_of(column, values)
703708
references = column_references([column])
704709
self.references_values |= references unless references.empty?
705710

706-
values = values.map { |value| type_caster.type_cast_for_database(column, value) }
711+
values = values.map { |value| model.type_caster.type_cast_for_database(column, value) }
707712
arel_column = column.is_a?(Arel::Nodes::SqlLiteral) ? column : order_column(column.to_s)
708713

709714
where_clause =
@@ -1914,7 +1919,7 @@ def arel_columns(columns)
19141919
case field
19151920
when Symbol
19161921
arel_column(field.to_s) do |attr_name|
1917-
adapter_class.quote_table_name(attr_name)
1922+
klass.adapter_class.quote_table_name(attr_name)
19181923
end
19191924
when String
19201925
arel_column(field, &:itself)
@@ -1946,7 +1951,7 @@ def arel_column(field)
19461951

19471952
def table_name_matches?(from)
19481953
table_name = Regexp.escape(table.name)
1949-
quoted_table_name = Regexp.escape(adapter_class.quote_table_name(table.name))
1954+
quoted_table_name = Regexp.escape(klass.adapter_class.quote_table_name(table.name))
19501955
/(?:\A|(?<!FROM)\s)(?:\b#{table_name}\b|#{quoted_table_name})(?!\.)/i.match?(from.to_s)
19511956
end
19521957

@@ -2081,7 +2086,7 @@ def order_column(field)
20812086
if attr_name == "count" && !group_values.empty?
20822087
table[attr_name]
20832088
else
2084-
Arel.sql(adapter_class.quote_table_name(attr_name), retryable: true)
2089+
Arel.sql(klass.adapter_class.quote_table_name(attr_name), retryable: true)
20852090
end
20862091
end
20872092
end

activerecord/lib/active_record/relation/record_fetch_warning.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ def exec_queries
2020
QueryRegistry.reset
2121

2222
super.tap do |records|
23-
if logger && ActiveRecord.warn_on_records_fetched_greater_than
23+
if model.logger && ActiveRecord.warn_on_records_fetched_greater_than
2424
if records.length > ActiveRecord.warn_on_records_fetched_greater_than
25-
logger.warn "Query fetched #{records.size} #{@klass} records: #{QueryRegistry.queries.join(";")}"
25+
model.logger.warn "Query fetched #{records.size} #{@klass} records: #{QueryRegistry.queries.join(";")}"
2626
end
2727
end
2828
end

activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -823,21 +823,6 @@ def test_association_proxy_transaction_method_starts_transaction_in_association_
823823
end
824824
end
825825

826-
def test_caching_of_columns
827-
david = Developer.find(1)
828-
# clear cache possibly created by other tests
829-
david.projects.reset_column_information
830-
831-
assert_queries_count(include_schema: true) { david.projects.columns }
832-
assert_no_queries { david.projects.columns }
833-
834-
## and again to verify that reset_column_information clears the cache correctly
835-
david.projects.reset_column_information
836-
837-
assert_queries_count(include_schema: true) { david.projects.columns }
838-
assert_no_queries { david.projects.columns }
839-
end
840-
841826
def test_attributes_are_being_set_when_initialized_from_habtm_association_with_where_clause
842827
new_developer = projects(:action_controller).developers.where(name: "Marcelo").build
843828
assert_equal "Marcelo", new_developer.name

0 commit comments

Comments
 (0)