Skip to content

Commit

Permalink
Fix prepared statements caching to be enabled even when query caching…
Browse files Browse the repository at this point in the history
… is enabled

Related cbcdecd, 2a56b2d.

This is a regression caused by cbcdecd.

If query caching is enabled, prepared statement handles are never
re-used, since we missed that a query is preprocessed when query caching
is enabled, but doesn't keep the `preparable` flag.

We should care about that case.
  • Loading branch information
kamipo committed Feb 25, 2019
1 parent f4bef91 commit 66b4ddd
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 15 deletions.
4 changes: 4 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,7 @@
* Fix prepared statements caching to be enabled even when query caching is enabled.

*Ryuta Kamizono*

* Ensure `update_all` series cares about optimistic locking.

*Ryuta Kamizono*
Expand Down
Expand Up @@ -20,9 +20,22 @@ def to_sql_and_binds(arel_or_sql_string, binds = []) # :nodoc:
raise "Passing bind parameters with an arel AST is forbidden. " \
"The values must be stored on the AST directly"
end
sql, binds = visitor.compile(arel_or_sql_string.ast, collector)
[sql.freeze, binds || []]

if prepared_statements
sql, binds = visitor.compile(arel_or_sql_string.ast, collector)

if binds.length > bind_params_length
unprepared_statement do
sql, binds = to_sql_and_binds(arel_or_sql_string)
visitor.preparable = false
end
end
else
sql = visitor.compile(arel_or_sql_string.ast, collector)
end
[sql.freeze, binds]
else
visitor.preparable = false if prepared_statements
[arel_or_sql_string.dup.freeze, binds]
end
end
Expand All @@ -47,13 +60,8 @@ def select_all(arel, name = nil, binds = [], preparable: nil)
arel = arel_from_relation(arel)
sql, binds = to_sql_and_binds(arel, binds)

if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
preparable = false
elsif binds.length > bind_params_length
sql, binds = unprepared_statement { to_sql_and_binds(arel) }
preparable = false
else
preparable = visitor.preparable
if preparable.nil?
preparable = prepared_statements ? visitor.preparable : false
end

if prepared_statements && preparable
Expand Down
Expand Up @@ -97,9 +97,8 @@ def select_all(arel, name = nil, binds = [], preparable: nil)
arel = arel_from_relation(arel)
sql, binds = to_sql_and_binds(arel, binds)

if binds.length > bind_params_length
sql, binds = unprepared_statement { to_sql_and_binds(arel) }
preparable = false
if preparable.nil?
preparable = prepared_statements ? visitor.preparable : false
end

cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) }
Expand Down
Expand Up @@ -3,7 +3,7 @@
module ActiveRecord
module ConnectionAdapters
module DetermineIfPreparableVisitor
attr_reader :preparable
attr_accessor :preparable

def accept(*)
@preparable = true
Expand Down
57 changes: 55 additions & 2 deletions activerecord/test/cases/bind_parameter_test.rb
Expand Up @@ -34,6 +34,49 @@ def teardown
ActiveSupport::Notifications.unsubscribe(@subscription)
end

def test_statement_cache
@connection.clear_cache!

topics = Topic.where(id: 1)
assert_equal [1], topics.map(&:id)
assert_includes statement_cache, to_sql_key(topics.arel)
end

def test_statement_cache_with_query_cache
@connection.enable_query_cache!
@connection.clear_cache!

topics = Topic.where(id: 1)
assert_equal [1], topics.map(&:id)
assert_includes statement_cache, to_sql_key(topics.arel)
ensure
@connection.disable_query_cache!
end

def test_statement_cache_with_find
@connection.clear_cache!

topics = Topic.where(id: 1).limit(1)
assert_equal 1, Topic.find(1).id
assert_includes statement_cache, to_sql_key(topics.arel)
end

def test_statement_cache_with_in_clause
@connection.clear_cache!

topics = Topic.where(id: [1, 3])
assert_equal [1, 3], topics.map(&:id)
assert_not_includes statement_cache, to_sql_key(topics.arel)
end

def test_statement_cache_with_sql_string_literal
@connection.clear_cache!

topics = Topic.where("topics.id = ?", 1)
assert_equal [1], topics.map(&:id)
assert_not_includes statement_cache, to_sql_key(topics.arel)
end

def test_too_many_binds
bind_params_length = @connection.send(:bind_params_length)

Expand All @@ -45,15 +88,16 @@ def test_too_many_binds
end

def test_too_many_binds_with_query_cache
Topic.connection.enable_query_cache!
@connection.enable_query_cache!

bind_params_length = @connection.send(:bind_params_length)
topics = Topic.where(id: (1 .. bind_params_length + 1).to_a)
assert_equal Topic.count, topics.count

topics = Topic.where.not(id: (1 .. bind_params_length + 1).to_a)
assert_equal 0, topics.count
ensure
Topic.connection.disable_query_cache!
@connection.disable_query_cache!
end

def test_bind_from_join_in_subquery
Expand Down Expand Up @@ -90,6 +134,15 @@ def test_logs_legacy_binds_after_type_cast
end

private
def to_sql_key(arel)
sql = @connection.to_sql(arel)
@connection.respond_to?(:sql_key, true) ? @connection.send(:sql_key, sql) : sql
end

def statement_cache
@connection.instance_variable_get(:@statements).send(:cache)
end

def assert_logs_binds(binds)
payload = {
name: "SQL",
Expand Down

0 comments on commit 66b4ddd

Please sign in to comment.