aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/CHANGELOG.md4
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/middleware/database_selector.rb22
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver.rb14
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver/session.rb2
-rw-r--r--activerecord/lib/active_record/railtie.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb6
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb12
-rw-r--r--activerecord/lib/active_record/scoping.rb8
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb3
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb15
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb4
-rw-r--r--activerecord/test/cases/finder_test.rb10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt9
14 files changed, 75 insertions, 40 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index e957361ec3..f16a746b91 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -29,7 +29,7 @@
```
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
- config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
```
To change the database selection strategy, pass a custom class to the
@@ -38,7 +38,7 @@
```
config.active_record.database_selector = { delay: 10.seconds }
config.active_record.database_resolver = MyResolver
- config.active_record.database_operations = MyResolver::MyCookies
+ config.active_record.database_resolver_context = MyResolver::MyCookies
```
*Eileen M. Uchitelle*
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 68f53d5c1c..b0c0beac0e 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -347,7 +347,6 @@ module ActiveRecord
add_to_target(record) do
result = insert_record(record, true, raise) {
@_was_loaded = loaded?
- @association_ids = nil
}
end
raise ActiveRecord::Rollback unless result
@@ -384,6 +383,7 @@ module ActiveRecord
delete_records(existing_records, method) if existing_records.any?
@target -= records
+ @association_ids = nil
records.each { |record| callback(:after_remove, record) }
end
@@ -424,7 +424,6 @@ module ActiveRecord
unless owner.new_record?
result &&= insert_record(record, true, raise) {
@_was_loaded = loaded?
- @association_ids = nil
}
end
end
@@ -447,6 +446,7 @@ module ActiveRecord
if index
target[index] = record
elsif @_was_loaded || !loaded?
+ @association_ids = nil
target << record
end
diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb
index 3ab50f5f6b..aad263695b 100644
--- a/activerecord/lib/active_record/middleware/database_selector.rb
+++ b/activerecord/lib/active_record/middleware/database_selector.rb
@@ -12,8 +12,8 @@ module ActiveRecord
#
# The resolver class defines when the application should switch (i.e. read
# from the primary if a write occurred less than 2 seconds ago) and an
- # operations class that sets a value that helps the resolver class decide
- # when to switch.
+ # resolver context class that sets a value that helps the resolver class
+ # decide when to switch.
#
# Rails default middleware uses the request's session to set a timestamp
# that informs the application when to read from a primary or read from a
@@ -24,7 +24,7 @@ module ActiveRecord
#
# config.active_record.database_selector = { delay: 2.seconds }
# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
- # config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
#
# New applications will include these lines commented out in the production.rb.
#
@@ -33,16 +33,16 @@ module ActiveRecord
#
# config.active_record.database_selector = { delay: 2.seconds }
# config.active_record.database_resolver = MyResolver
- # config.active_record.database_operations = MyResolver::MySession
+ # config.active_record.database_resolver_context = MyResolver::MySession
class DatabaseSelector
- def initialize(app, resolver_klass = Resolver, operations_klass = Resolver::Session, options = {})
+ def initialize(app, resolver_klass = Resolver, context_klass = Resolver::Session, options = {})
@app = app
@resolver_klass = resolver_klass
- @operations_klass = operations_klass
+ @context_klass = context_klass
@options = options
end
- attr_reader :resolver_klass, :operations_klass, :options
+ attr_reader :resolver_klass, :context_klass, :options
# Middleware that determines which database connection to use in a multiple
# database application.
@@ -57,13 +57,13 @@ module ActiveRecord
private
def select_database(request, &blk)
- operations = operations_klass.build(request)
- database_resolver = resolver_klass.call(operations, options)
+ context = context_klass.call(request)
+ resolver = resolver_klass.call(context, options)
if reading_request?(request)
- database_resolver.read(&blk)
+ resolver.read(&blk)
else
- database_resolver.write(&blk)
+ resolver.write(&blk)
end
end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
index a84c292714..80b8cd7cae 100644
--- a/activerecord/lib/active_record/middleware/database_selector/resolver.rb
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
@@ -18,18 +18,18 @@ module ActiveRecord
class Resolver # :nodoc:
SEND_TO_REPLICA_DELAY = 2.seconds
- def self.call(resolver, options = {})
- new(resolver, options)
+ def self.call(context, options = {})
+ new(context, options)
end
- def initialize(resolver, options = {})
- @resolver = resolver
+ def initialize(context, options = {})
+ @context = context
@options = options
@delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
@instrumenter = ActiveSupport::Notifications.instrumenter
end
- attr_reader :resolver, :delay, :instrumenter
+ attr_reader :context, :delay, :instrumenter
def read(&blk)
if read_from_primary?
@@ -68,7 +68,7 @@ module ActiveRecord
instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
yield
ensure
- resolver.update_last_write_timestamp
+ context.update_last_write_timestamp
end
end
end
@@ -82,7 +82,7 @@ module ActiveRecord
end
def time_since_last_write_ok?
- Time.now - resolver.last_write_timestamp >= send_to_replica_delay
+ Time.now - context.last_write_timestamp >= send_to_replica_delay
end
end
end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
index 33e0af5ee4..df7af054b7 100644
--- a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
@@ -10,7 +10,7 @@ module ActiveRecord
# The last_write is used to determine whether it's safe to read
# from the replica or the request needs to be sent to the primary.
class Session # :nodoc:
- def self.build(request)
+ def self.call(request)
new(request.session)
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index aac49a92b4..a1d7c893bf 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -91,7 +91,7 @@ module ActiveRecord
initializer "active_record.database_selector" do
if options = config.active_record.delete(:database_selector)
resolver = config.active_record.delete(:database_resolver)
- operations = config.active_record.delete(:database_operations)
+ operations = config.active_record.delete(:database_resolver_context)
config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 32f0609798..347d745d19 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -67,11 +67,7 @@ module ActiveRecord
# user = users.new { |user| user.name = 'Oscar' }
# user.name # => Oscar
def new(attributes = nil, &block)
- current_scope = klass.current_scope(true)
- block = -> record do
- klass.current_scope = current_scope
- yield record if block_given?
- end
+ block = klass.current_scope_restoring_block(&block)
scoping { klass.new(attributes, &block) }
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 8f1065c1e7..91cfc4e849 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -307,8 +307,6 @@ module ActiveRecord
return false if !conditions || limit_value == 0
- conditions = sanitize_forbidden_attributes(conditions)
-
if eager_loading?
relation = apply_join_dependency(eager_loading: false)
return relation.exists?(conditions)
@@ -316,7 +314,7 @@ module ActiveRecord
relation = construct_relation_for_exists(conditions)
- skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false
+ skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists") } ? true : false
end
# This method is called whenever no records are found with either a single
@@ -354,7 +352,13 @@ module ActiveRecord
end
def construct_relation_for_exists(conditions)
- relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
+ conditions = sanitize_forbidden_attributes(conditions)
+
+ if distinct_value && offset_value
+ relation = limit(1)
+ else
+ relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
+ end
case conditions
when Array, Hash
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 35e9dcbffc..1142a87d25 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -30,6 +30,14 @@ module ActiveRecord
def current_scope=(scope)
ScopeRegistry.set_value_for(:current_scope, self, scope)
end
+
+ def current_scope_restoring_block(&block)
+ current_scope = self.current_scope(true)
+ -> *args do
+ self.current_scope = current_scope
+ yield(*args) if block_given?
+ end
+ end
end
def populate_with_current_scope_attributes # :nodoc:
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index a61569420e..c01138c059 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -63,7 +63,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
Client.find(3).firm
ensure
- assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query"
+ sql_log = ActiveRecord::SQLCounter.log
+ assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
end
def test_belongs_to_with_primary_key
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 5fdc5a92fc..bf44be1811 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -2004,6 +2004,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_not_predicate company.clients, :loaded?
end
+ def test_ids_reader_cache_not_used_for_size_when_association_is_dirty
+ firm = Firm.create!(name: "Startup")
+ assert_equal 0, firm.client_ids.size
+ firm.clients.build
+ assert_equal 1, firm.clients.size
+ end
+
+ def test_ids_reader_cache_should_be_cleared_when_collection_is_deleted
+ firm = companies(:first_firm)
+ assert_equal [2, 3, 11], firm.client_ids
+ client = firm.clients.first
+ firm.clients.delete(client)
+ assert_equal [3, 11], firm.client_ids
+ end
+
def test_zero_counter_cache_usage_on_unloaded_association
car = Car.create!(name: "My AppliCar")
assert_no_queries do
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 3e5b5c1275..7bb629466d 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -37,8 +37,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
companies(:first_firm).account
ensure
- log_all = ActiveRecord::SQLCounter.log_all
- assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}"
+ sql_log = ActiveRecord::SQLCounter.log
+ assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
end
def test_has_one_cache_nils
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 6af2a43c7f..4040682280 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -271,6 +271,16 @@ class FinderTest < ActiveRecord::TestCase
assert_equal true, Topic.exists?({})
end
+ def test_exists_with_distinct_and_offset_and_joins
+ assert Post.left_joins(:comments).distinct.offset(10).exists?
+ assert_not Post.left_joins(:comments).distinct.offset(11).exists?
+ end
+
+ def test_exists_with_distinct_and_offset_and_select
+ assert Post.select(:body).distinct.offset(3).exists?
+ assert_not Post.select(:body).distinct.offset(4).exists?
+ end
+
# Ensure +exists?+ runs without an error by excluding distinct value.
# See https://github.com/rails/rails/pull/26981.
def test_exists_with_order_and_distinct
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
index 94f7dd0c79..ed1cf09eeb 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
@@ -107,9 +107,10 @@ Rails.application.configure do
# The `database_resolver` class is used by the middleware to determine which
# database is appropriate to use based on the time delay.
#
- # The `database_operations` class is used by the middleware to set timestamps
- # for the last write to the primary. The resolver uses the operations class
- # timestamps to determine how long to wait before reading from the replica.
+ # The `database_resolver_context` class is used by the middleware to set
+ # timestamps for the last write to the primary. The resolver uses the context
+ # class timestamps to determine how long to wait before reading from the
+ # replica.
#
# By default Rails will store a last write timestamp in the session. The
# DatabaseSelector middleware is designed as such you can define your own
@@ -117,5 +118,5 @@ Rails.application.configure do
# these configuration options.
# config.active_record.database_selector = { delay: 2.seconds }
# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
- # config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end