1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
|
module ActiveRecord
# Statement cache is used to cache a single statement in order to avoid creating the AST again.
# Initializing the cache is done by passing the statement in the create block:
#
# cache = StatementCache.create(Book.connection) do |params|
# Book.where(name: "my book").where("author_id > 3")
# end
#
# The cached statement is executed by using the +execute+ method:
#
# cache.execute([], Book, Book.connection)
#
# 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.
#
# If you want to cache the statement without the values you can use the +bind+ method of the
# block parameter.
#
# cache = StatementCache.create(Book.connection) do |params|
# Book.where(name: params.bind)
# end
#
# And pass the bind values as the first argument of +execute+ call.
#
# cache.execute(["my book"], Book, Book.connection)
class StatementCache # :nodoc:
class Substitute; end # :nodoc:
class Query # :nodoc:
def initialize(sql)
@sql = sql
end
def sql_for(binds, connection)
@sql
end
end
class PartialQuery < Query # :nodoc:
def initialize values
@values = values
@indexes = values.each_with_index.find_all { |thing,i|
Arel::Nodes::BindParam === thing
}.map(&:last)
end
def sql_for(binds, connection)
val = @values.dup
binds = binds.dup
@indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) }
val.join
end
end
def self.query(visitor, ast)
Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value
end
def self.partial_query(visitor, ast, collector)
collected = visitor.accept(ast, collector).value
PartialQuery.new collected
end
class Params # :nodoc:
def bind; Substitute.new; end
end
class BindMap # :nodoc:
def initialize(bind_values)
@indexes = []
@bind_values = bind_values
bind_values.each_with_index do |(_, value), i|
if Substitute === value
@indexes << i
end
end
end
def bind(values)
bvs = @bind_values.map { |pair| pair.dup }
@indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] }
bvs
end
end
attr_reader :bind_map, :query_builder
def self.create(connection, block = Proc.new)
relation = block.call Params.new
bind_map = BindMap.new relation.bind_values
query_builder = connection.cacheable_query relation.arel
new query_builder, bind_map
end
def initialize(query_builder, bind_map)
@query_builder = query_builder
@bind_map = bind_map
end
def execute(params, klass, connection)
bind_values = bind_map.bind params
sql = query_builder.sql_for bind_values, connection
klass.find_by_sql sql, bind_values
end
alias :call :execute
end
end
|