# frozen_string_literal: true 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(ClothingItem.lease_connection) do |params| # Book.where(name: "my book").where("author_id > 3") # end # # The cached statement is executed by using the # {connection.execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] method: # # cache.execute([], ClothingItem.lease_connection) # # The relation returned by the block is cached, and for each # {execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#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(ClothingItem.lease_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"], ClothingItem.lease_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| Substitute === thing }.map(&:last) end def sql_for(binds, connection) val = @values.dup @indexes.each do |i| value = binds.shift if ActiveModel::Attribute === value value = value.value_for_database end val[i] = connection.quote(value) end val.join end end class PartialQueryCollector attr_accessor :preparable, :retryable def initialize @parts = [] @binds = [] end def <<(str) @parts << str self end def add_bind(obj) @binds << obj @parts << Substitute.new self end def add_binds(binds, proc_for_binds = nil) @binds.concat proc_for_binds ? binds.map(&proc_for_binds) : binds binds.size.times do |i| @parts << ", " unless i == 0 @parts << Substitute.new end self end def value [@parts, @binds] end end def self.query(sql) Query.new(sql) end def self.partial_query(values) PartialQuery.new(values) end def self.partial_query_collector PartialQueryCollector.new end class Params # :nodoc: def bind; Substitute.new; end end class BindMap # :nodoc: def initialize(bound_attributes) @indexes = [] @bound_attributes = bound_attributes bound_attributes.each_with_index do |attr, i| if ActiveModel::Attribute === attr && Substitute === attr.value @indexes << i end end end def bind(values) bas = @bound_attributes.dup @indexes.each_with_index { |offset, i| bas[offset] = bas[offset].with_cast_value(values[i]) } bas end end def self.create(connection, callable = nil, &block) relation = (callable || block).call Params.new query_builder, binds = connection.cacheable_query(self, relation.arel) bind_map = BindMap.new(binds) new(query_builder, bind_map, relation.klass) end def initialize(query_builder, bind_map, klass) @query_builder = query_builder @bind_map = bind_map @klass = klass end def execute(params, connection, allow_retry: false, &block) bind_values = bind_map.bind params sql = query_builder.sql_for bind_values, connection klass.find_by_sql(sql, bind_values, preparable: true, allow_retry: allow_retry, &block) rescue ::RangeError [] end def self.unsupported_value?(value) case value when NilClass, Array, Range, Hash, Relation, Base then true end end private attr_reader :query_builder, :bind_map, :klass end end