aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb1
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb8
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb19
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb11
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb6
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb3
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb55
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb3
-rw-r--r--activerecord/lib/active_record/statement_cache.rb97
15 files changed, 191 insertions, 33 deletions
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 17f056e764..f455b6934e 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -95,6 +95,7 @@ module ActiveRecord
end
scope.where_values += item.where_values
+ scope.bind_values += item.bind_values
scope.order_values |= item.order_values
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 295dccf34e..b5f9ee6cee 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -164,17 +164,17 @@ module ActiveRecord
def make_outer_joins(parent, child)
tables = table_aliases_for(parent, child)
join_type = Arel::OuterJoin
- joins = make_constraints parent, child, tables, join_type
+ info = make_constraints parent, child, tables, join_type
- joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
+ [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
end
def make_inner_joins(parent, child)
tables = child.tables
join_type = Arel::InnerJoin
- joins = make_constraints parent, child, tables, join_type
+ info = make_constraints parent, child, tables, join_type
- joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
+ [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
end
def table_aliases_for(parent, node)
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 0cd2e1a816..0333816abb 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -21,8 +21,11 @@ module ActiveRecord
super && reflection == other.reflection
end
+ JoinInformation = Struct.new :joins, :binds
+
def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
joins = []
+ bind_values = []
tables = tables.reverse
scope_chain_iter = scope_chain.reverse_each
@@ -58,21 +61,27 @@ module ActiveRecord
left.merge right
end
- if reflection.type
- constraint = constraint.and table[reflection.type].eq foreign_klass.base_class.name
- end
-
if rel && !rel.arel.constraints.empty?
+ bind_values.concat rel.bind_values
constraint = constraint.and rel.arel.constraints
end
+ if reflection.type
+ value = foreign_klass.base_class.name
+ column = klass.columns_hash[column.to_s]
+
+ substitute = klass.connection.substitute_at(column, bind_values.length)
+ bind_values.push [column, value]
+ constraint = constraint.and table[reflection.type].eq substitute
+ end
+
joins << table.create_join(table, table.create_on(constraint), join_type)
# The current table in this iteration becomes the foreign table in the next
foreign_table, foreign_klass = table, klass
end
- joins
+ JoinInformation.new joins, bind_values
end
# Builds equality condition.
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 83637a0409..e49fc5d5c4 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -80,7 +80,7 @@ module ActiveRecord
# { author: :avatar }
# [ :books, { author: :avatar } ]
- NULL_RELATION = Struct.new(:values).new({})
+ NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact.uniq
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 69b65982b3..83c69586e6 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -111,10 +111,13 @@ module ActiveRecord
scope = klass.unscoped
values = reflection_scope.values
+ reflection_binds = reflection_scope.bind_values
preload_values = preload_scope.values
+ preload_binds = preload_scope.bind_values
scope.where_values = Array(values[:where]) + Array(preload_values[:where])
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
+ scope.bind_values = (reflection_binds + preload_binds)
scope.select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index c90915c509..cc108a3a61 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -18,6 +18,12 @@ module ActiveRecord
end
end
+ # This is used in the StatementCache object. It returns an object that
+ # can be used to query the database repeatedly.
+ def cacheable_query(arel) # :nodoc:
+ ActiveRecord::StatementCache.query visitor, arel.ast
+ end
+
# Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [])
select(to_sql(arel, binds), name, binds)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 3c94bad208..a8a530e1d5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -104,6 +104,10 @@ module ActiveRecord
@prepared_statements = false
end
+ def bind_substitution_visitor
+ @bind_sub_visitor ||= visitor.dup.extend(Arel::Visitors::BindVisitor)
+ end
+
def valid_type?(type)
true
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 6d8e994654..830b1be021 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -44,6 +44,10 @@ module ActiveRecord
configure_connection
end
+ def cacheable_query(arel)
+ ActiveRecord::StatementCache.partial_query visitor, arel.ast
+ end
+
MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191
def initialize_schema_migrations_table
if @config[:encoding] == 'utf8mb4'
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 170dddb08e..55533311f5 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -273,7 +273,7 @@ module ActiveRecord
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', []))
end
class ExplainPrettyPrinter
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 745c6cf349..770b48c550 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -222,6 +222,7 @@ module ActiveRecord
# Please see further details in the
# {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
+ #TODO: Fix for binds.
exec_explain(collecting_queries_for_explain { exec_queries })
end
@@ -509,15 +510,15 @@ module ActiveRecord
@to_sql ||= begin
relation = self
connection = klass.connection
- visitor = connection.visitor
+ visitor = connection.bind_substitution_visitor
if eager_loading?
find_with_associations { |rel| relation = rel }
end
- ast = relation.arel.ast
- binds = relation.bind_values.dup
- visitor.accept(ast) do
+ arel = relation.arel
+ binds = arel.bind_values + relation.bind_values
+ visitor.accept(arel.ast) do
connection.quote(*binds.shift.reverse)
end
end
@@ -599,7 +600,7 @@ module ActiveRecord
private
def exec_queries
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
preload = preload_values
preload += includes_values unless eager_loading?
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 45ffb99868..6e868902c5 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -235,11 +235,14 @@ module ActiveRecord
column_alias = column_name
+ bind_values = nil
+
if operation == "count" && (relation.limit_value || relation.offset_value)
# Shortcut when limit is zero.
return 0 if relation.limit_value == 0
query_builder = build_count_subquery(relation, column_name, distinct)
+ bind_values = relation.bind_values
else
column = aggregate_column(column_name)
@@ -249,9 +252,10 @@ module ActiveRecord
relation.select_values = [select_value]
query_builder = relation.arel
+ bind_values = query_builder.bind_values + relation.bind_values
end
- result = @klass.connection.select_all(query_builder, nil, relation.bind_values)
+ result = @klass.connection.select_all(query_builder, nil, bind_values)
row = result.first
value = row && row.values.first
column = result.column_types.fetch(column_alias) do
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 4984dbd277..64ac265689 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -255,7 +255,8 @@ module ActiveRecord
if ActiveRecord::NullRelation === relation
[]
else
- rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
+ arel = relation.arel
+ rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
join_dependency.instantiate(rows, aliases)
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 979216bee7..6cf57b679d 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -827,7 +827,7 @@ module ActiveRecord
build_joins(arel, joins_values.flatten) unless joins_values.empty?
- collapse_wheres(arel, (where_values - ['']).uniq)
+ collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds
arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
@@ -844,6 +844,15 @@ module ActiveRecord
arel.from(build_from) if from_value
arel.lock(lock_value) if lock_value
+ # Reorder bind indexes if joins produced bind values
+ if arel.bind_values.any?
+ bvs = arel.bind_values + bind_values
+ arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i|
+ column = bvs[i].first
+ bp.replace connection.substitute_at(column, i)
+ end
+ end
+
arel
end
@@ -861,6 +870,8 @@ module ActiveRecord
when :order
self.reverse_order_value = false
result = []
+ when :where
+ self.bind_values = []
else
result = [] unless single_val_method
end
@@ -913,18 +924,16 @@ module ActiveRecord
def build_where(opts, other = [])
case opts
when String, Array
- #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113
- values = Hash === other.first ? other.first.values : other
-
- values.grep(ActiveRecord::Relation) do |rel|
- self.bind_values += rel.bind_values
- end
-
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
opts = PredicateBuilder.resolve_column_aliases(klass, opts)
attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
+ bv_len = bind_values.length
+ tmp_opts, bind_values = create_binds(opts, bv_len)
+ self.bind_values += bind_values
+
+ attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
attributes.values.grep(ActiveRecord::Relation) do |rel|
self.bind_values += rel.bind_values
end
@@ -935,6 +944,29 @@ module ActiveRecord
end
end
+ def create_binds(opts, idx)
+ bindable, non_binds = opts.partition do |column, value|
+ case value
+ when String, Integer, ActiveRecord::StatementCache::Substitute
+ @klass.columns_hash.include? column.to_s
+ else
+ false
+ end
+ end
+
+ new_opts = {}
+ binds = []
+
+ bindable.each_with_index do |(column,value), index|
+ binds.push [@klass.columns_hash[column.to_s], value]
+ new_opts[column] = connection.substitute_at(column, index + idx)
+ end
+
+ non_binds.each { |column,value| new_opts[column] = value }
+
+ [new_opts, binds]
+ end
+
def build_from
opts, name = from_value
case opts
@@ -976,9 +1008,12 @@ module ActiveRecord
join_list
)
- joins = join_dependency.join_constraints stashed_association_joins
+ join_infos = join_dependency.join_constraints stashed_association_joins
- joins.each { |join| manager.from(join) }
+ join_infos.each do |info|
+ info.joins.each { |join| manager.from(join) }
+ manager.bind_values.concat info.binds
+ end
manager.join_sources.concat(join_list)
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 2552cbd234..57d66bce4b 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -58,6 +58,9 @@ module ActiveRecord
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
def only(*onlies)
+ if onlies.any? { |o| o == :where }
+ onlies << :bind
+ end
relation_with values.slice(*onlies)
end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index dd4ee0c4a0..6090dfeb13 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -14,13 +14,100 @@ module ActiveRecord
# The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped.
# Database is queried when +to_a+ is called on the relation.
class StatementCache
- def initialize
- @relation = yield
- raise ArgumentError.new("Statement cannot be nil") if @relation.nil?
+ Substitute = Struct.new :name
+
+ class Query
+ def initialize(sql)
+ @sql = sql
+ end
+
+ def sql_for(binds, connection)
+ @sql
+ end
+ end
+
+ class PartialQuery < Query
+ def sql_for(binds, connection)
+ @sql.gsub(/\?/) { connection.quote(*binds.shift.reverse) }
+ end
+ end
+
+ def self.query(visitor, ast)
+ Query.new visitor.accept(ast)
+ end
+
+ def self.partial_query(visitor, ast)
+ sql = visitor.accept(ast) { "?" }
+ PartialQuery.new sql
+ end
+
+ class Params
+ def [](name); Substitute.new name; end
+ end
+
+ class BindMap
+ def initialize(bind_values)
+ @value_map = {}
+ @bind_values = bind_values
+
+ bind_values.each_with_index do |(_, value), i|
+ if Substitute === value
+ @value_map[value.name] = i
+ end
+ end
+ end
+
+ def bind(values)
+ bvs = @bind_values.map { |pair| pair.dup }
+ values.each { |k,v| bvs[@value_map[k]][1] = v }
+ bvs
+ end
+ end
+
+ def initialize(block = Proc.new)
+ @mutex = Mutex.new
+ @relation = nil
+ @sql = nil
+ @binds = nil
+ @block = block
+ @query_builder = nil
+ @params = Params.new
+ end
+
+ def execute(params)
+ rel = relation @params
+
+ arel = rel.arel
+ klass = rel.klass
+ bind_map = binds rel
+ bind_values = bind_map.bind params
+
+ builder = query_builder klass.connection, arel
+ sql = builder.sql_for bind_values, klass.connection
+
+ klass.find_by_sql sql, bind_values
+ end
+ alias :call :execute
+
+ private
+ def binds(rel)
+ @binds || @mutex.synchronize { @binds ||= BindMap.new rel.bind_values }
+ end
+
+ def query_builder(connection, arel)
+ @query_builder || @mutex.synchronize {
+ @query_builder ||= connection.cacheable_query(arel)
+ }
+ end
+
+ def sql(klass, arel, bv)
+ @sql || @mutex.synchronize {
+ @sql ||= klass.connection.to_sql arel, bv
+ }
end
- def execute
- @relation.dup.to_a
+ def relation(values)
+ @relation || @mutex.synchronize { @relation ||= @block.call(values) }
end
end
end