diff options
Diffstat (limited to 'activerecord')
17 files changed, 112 insertions, 53 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 654caafc92..c412646cc9 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,9 @@ +* Chaining named scope is no longer leaking to class level querying methods. + + Fixes #14003. + + *Ryuta Kamizono* + * Allow applications to automatically switch connections. Adds a middleware and configuration options that can be used in your diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 013e81c959..9824787658 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -89,18 +89,23 @@ end namespace :db do namespace :mysql do + connection_arguments = lambda do |connection_name| + config = ARTest.config["connections"]["mysql2"][connection_name] + ["--user=#{config["username"]}", "--password=#{config["password"]}", ("--host=#{config["host"]}" if config["host"])].join(" ") + end + desc "Build the MySQL test databases" task :build do config = ARTest.config["connections"]["mysql2"] - %x( mysql --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" ) - %x( mysql --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" ) + %x( mysql #{connection_arguments["arunit"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" ) + %x( mysql #{connection_arguments["arunit2"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" ) end desc "Drop the MySQL test databases" task :drop do config = ARTest.config["connections"]["mysql2"] - %x( mysqladmin --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -f drop #{config["arunit"]["database"]} ) - %x( mysqladmin --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -f drop #{config["arunit2"]["database"]} ) + %x( mysqladmin #{connection_arguments["arunit"]} -f drop #{config["arunit"]["database"]} ) + %x( mysqladmin #{connection_arguments["arunit2"]} -f drop #{config["arunit2"]["database"]} ) end desc "Rebuild the MySQL test databases" diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 4a25567c9d..6f5df807fe 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -210,7 +210,8 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if !find_target? || loaded? + if !find_target? + loaded! unless loaded? target.size elsif @association_ids @association_ids.size @@ -233,7 +234,7 @@ module ActiveRecord # loaded and you are going to fetch the records anyway it is better to # check <tt>collection.length.zero?</tt>. def empty? - if loaded? || @association_ids + if loaded? || @association_ids || reflection.has_cached_counter? size.zero? else target.empty? && !scope.exists? diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index eb22db838c..6f67934a79 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -36,14 +36,6 @@ module ActiveRecord super end - def empty? - if reflection.has_cached_counter? - size.zero? - else - super - end - end - private # Returns the number of records in this collection. @@ -60,20 +52,24 @@ module ActiveRecord # If the collection is empty the target is set to an empty array and # the loaded flag is set to true as well. def count_records - count = if reflection.has_cached_counter? - owner._read_attribute(reflection.counter_cache_column).to_i - else - scope.count(:all) - end + count = counter_cache_value || scope.count(:all) # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a # documented side-effect of the method that may avoid an extra SELECT. - (@target ||= []) && loaded! if count == 0 + loaded! if count == 0 [association_scope.limit_value, count].compact.min end + def counter_cache_value + reflection.has_cached_counter? ? owner._read_attribute(reflection.counter_cache_column).to_i : nil + end + + def find_target? + super && !counter_cache_value&.zero? + end + def update_counter(difference, reflection = reflection()) if reflection.has_cached_counter? owner.increment!(reflection.counter_cache_column, difference) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 14dbd20bcd..7b3630662b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -9,7 +9,7 @@ require "active_record/connection_adapters/sqlite3/schema_definitions" require "active_record/connection_adapters/sqlite3/schema_dumper" require "active_record/connection_adapters/sqlite3/schema_statements" -gem "sqlite3", "~> 1.3.6" +gem "sqlite3", "~> 1.3", ">= 1.3.6" require "sqlite3" module ActiveRecord diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 84604452f7..c67980173f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -477,6 +477,14 @@ module ActiveRecord end end + def present? # :nodoc: + true + end + + def blank? # :nodoc: + false + end + # Returns +true+ if the record is read only. Records loaded through joins with piggy-back # attributes will be marked as read only since they cannot be saved. def readonly? diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index a863227276..bab00ef065 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -312,7 +312,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @delegate_to_klass ? yield : klass._scoping(self) { yield } + @delegate_to_klass && klass.current_scope ? yield : klass._scoping(self) { yield } end def _exec_scope(*args, &block) # :nodoc: diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 6e8a1fcad4..f7c3b3783f 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -132,7 +132,7 @@ module ActiveRecord private def respond_to_missing?(method, _) - super || @klass.respond_to?(method) || arel.respond_to?(method) + super || @klass.respond_to?(method) end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 7874c4c35a..d758e289ca 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -8,7 +8,7 @@ module ActiveRecord module SpawnMethods # This is overridden by Associations::CollectionProxy def spawn #:nodoc: - @delegate_to_klass ? klass.all : clone + @delegate_to_klass && klass.current_scope ? klass.all : clone end # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 9eba1254a4..c3a56b2174 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -23,11 +23,11 @@ module ActiveRecord current_scope end - private - def current_scope(skip_inherited_scope = false) - ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) - end + def current_scope(skip_inherited_scope = false) + ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) + end + private def current_scope=(scope) ScopeRegistry.set_value_for(:current_scope, self, scope) end @@ -84,7 +84,7 @@ module ActiveRecord base = model.base_class while klass <= base value = @registry[scope_type][klass.name] - return value if value + return value || nil unless value.nil? klass = klass.superclass end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 5278eb29a9..987e6bd63f 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -180,7 +180,8 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.define_method(name) do |*args| - scope = all._exec_scope(*args, &body) + scope = all + scope = _scoping(false) { scope._exec_scope(*args, &body) } scope = scope.extending(extension) if extension scope end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 2f090d9862..4c9e4d0ad2 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1224,12 +1224,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_has_many_without_counter_cache_option # Ship has a conventionally named `treasures_count` column, but the counter_cache # option is not given on the association. - ship = Ship.create(name: "Countless", treasures_count: 10) + ship = Ship.create!(name: "Countless", treasures_count: 10) assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter? # Count should come from sql count() of treasures rather than treasures_count attribute - assert_equal ship.treasures.size, 0 + assert_queries(1) do + assert_equal ship.treasures.size, 0 + assert_predicate ship.treasures, :loaded? + end assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do ship.treasures.create(name: "Gold") @@ -1350,6 +1353,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) assert_no_queries do assert_not_empty post.comments + assert_equal 2, post.comments.size + assert_not_predicate post.comments, :loaded? + end + post = posts(:misc_by_bob) + assert_no_queries do + assert_empty post.comments + assert_predicate post.comments, :loaded? + end + end + + def test_empty_association_loading_with_counter_cache + post = posts(:misc_by_bob) + assert_no_queries do + assert_empty post.comments.to_a end end @@ -1986,9 +2003,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_not_predicate company.clients, :loaded? end - def test_counter_cache_on_unloaded_association - car = Car.create(name: "My AppliCar") - assert_equal car.engines.size, 0 + def test_zero_counter_cache_usage_on_unloaded_association + car = Car.create!(name: "My AppliCar") + assert_no_queries do + assert_equal car.engines.size, 0 + assert_predicate car.engines, :loaded? + end + end + + def test_counter_cache_on_new_record_unloaded_association + car = Car.new(name: "My AppliCar") + # Ensure no schema queries inside assertion + Engine.primary_key + assert_no_queries do + assert_equal car.engines.size, 0 + assert_predicate car.engines, :loaded? + end end def test_get_ids_ignores_include_option diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index b600c999a6..63ae438de3 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -5,7 +5,7 @@ require "models/post" require "models/comment" module ActiveRecord - module ArrayDelegationTests + module DelegationTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, @@ -21,10 +21,14 @@ module ActiveRecord assert_respond_to target, method end end + + def test_not_respond_to_arel_method + assert_not_respond_to target, :exists + end end class DelegationAssociationTest < ActiveRecord::TestCase - include ArrayDelegationTests + include DelegationTests def target Post.new.comments @@ -32,7 +36,7 @@ module ActiveRecord end class DelegationRelationTest < ActiveRecord::TestCase - include ArrayDelegationTests + include DelegationTests def target Comment.all diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 756eeca35f..6b5b877260 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -480,21 +480,6 @@ class RelationTest < ActiveRecord::TestCase assert_nothing_raised { Topic.reorder([]) } end - def test_respond_to_delegates_to_arel - relation = Topic.all - fake_arel = Struct.new(:responds) { - def respond_to?(method, access = false) - responds << [method, access] - end - }.new [] - - relation.extend(Module.new { attr_accessor :arel }) - relation.arel = fake_arel - - relation.respond_to?(:matching_attributes) - assert_equal [:matching_attributes, false], fake_arel.responds.first - end - def test_respond_to_dynamic_finders relation = Topic.all diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 418a2ae04e..27f9df295f 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -447,6 +447,16 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq end + def test_chaining_doesnt_leak_conditions_to_another_scopes + expected = Topic.where(approved: false).where(id: Topic.children.select(:parent_id)) + assert_equal expected.to_a, Topic.rejected.has_children.to_a + end + + def test_nested_scoping + expected = Reply.approved + assert_equal expected.to_a, Topic.rejected.nested_scoping(expected) + end + def test_scopes_batch_finders assert_equal 4, Topic.approved.count diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 33962f9e5e..f5e3ac3c19 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -54,10 +54,16 @@ connections: username: rails encoding: utf8mb4 collation: utf8mb4_unicode_ci +<% if ENV['MYSQL_HOST'] %> + host: <%= ENV['MYSQL_HOST'] %> +<% end %> arunit2: username: rails encoding: utf8mb4 collation: utf8mb4_general_ci +<% if ENV['MYSQL_HOST'] %> + host: <%= ENV['MYSQL_HOST'] %> +<% end %> oracle: arunit: diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index a6a47687a2..fdb461ed7f 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -10,6 +10,9 @@ class Topic < ActiveRecord::Base scope :approved, -> { where(approved: true) } scope :rejected, -> { where(approved: false) } + scope :children, -> { where.not(parent_id: nil) } + scope :has_children, -> { where(id: Topic.children.select(:parent_id)) } + scope :scope_with_lambda, lambda { all } scope :by_lifo, -> { where(author_name: "lifo") } @@ -88,6 +91,10 @@ class Topic < ActiveRecord::Base write_attribute(:approved, val) end + def self.nested_scoping(scope) + scope.base + end + private def default_written_on |