aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/statement_cache.rb
blob: 3047a81ec4481981a58f80b638d2a73062e19e43 (plain) (blame)
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 = connection.prepare_binds_for_database(binds)
        @indexes.each { |i| val[i] = connection.quote(binds.shift.last) }
        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(&: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