aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases/query_cache_test.rb
blob: d84653e4c9c8d5337df79d8db0a32f038c62016a (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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
require "cases/helper"
require 'models/topic'
require 'models/task'
require 'models/category'
require 'models/post'
require 'rack'

class QueryCacheTest < ActiveRecord::TestCase
  fixtures :tasks, :topics, :categories, :posts, :categories_posts

  teardown do
    Task.connection.clear_query_cache
    ActiveRecord::Base.connection.disable_query_cache!
  end

  def test_exceptional_middleware_clears_and_disables_cache_on_error
    assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'

    mw = ActiveRecord::QueryCache.new lambda { |env|
      Task.find 1
      Task.find 1
      assert_equal 1, ActiveRecord::Base.connection.query_cache.length
      raise "lol borked"
    }
    assert_raises(RuntimeError) { mw.call({}) }

    assert_equal 0, ActiveRecord::Base.connection.query_cache.length
    assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
  end

  def test_exceptional_middleware_leaves_enabled_cache_alone
    ActiveRecord::Base.connection.enable_query_cache!

    mw = ActiveRecord::QueryCache.new lambda { |env|
      raise "lol borked"
    }
    assert_raises(RuntimeError) { mw.call({}) }

    assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
  end

  def test_exceptional_middleware_assigns_original_connection_id_on_error
    connection_id = ActiveRecord::Base.connection_id

    mw = ActiveRecord::QueryCache.new lambda { |env|
      ActiveRecord::Base.connection_id = self.object_id
      raise "lol borked"
    }
    assert_raises(RuntimeError) { mw.call({}) }

    assert_equal connection_id, ActiveRecord::Base.connection_id
  end

  def test_middleware_delegates
    called = false
    mw = ActiveRecord::QueryCache.new lambda { |env|
      called = true
      [200, {}, nil]
    }
    mw.call({})
    assert called, 'middleware should delegate'
  end

  def test_middleware_caches
    mw = ActiveRecord::QueryCache.new lambda { |env|
      Task.find 1
      Task.find 1
      assert_equal 1, ActiveRecord::Base.connection.query_cache.length
      [200, {}, nil]
    }
    mw.call({})
  end

  def test_cache_enabled_during_call
    assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'

    mw = ActiveRecord::QueryCache.new lambda { |env|
      assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
      [200, {}, nil]
    }
    mw.call({})
  end

  def test_cache_on_during_body_write
    streaming = Class.new do
      def each
        yield ActiveRecord::Base.connection.query_cache_enabled
      end
    end

    mw = ActiveRecord::QueryCache.new lambda { |env|
      [200, {}, streaming.new]
    }
    body = mw.call({}).last
    body.each { |x| assert x, 'cache should be on' }
    body.close
    assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled'
  end

  def test_cache_off_after_close
    mw = ActiveRecord::QueryCache.new lambda { |env| [200, {}, nil] }
    body = mw.call({}).last

    assert ActiveRecord::Base.connection.query_cache_enabled, 'cache enabled'
    body.close
    assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled'
  end

  def test_cache_clear_after_close
    mw = ActiveRecord::QueryCache.new lambda { |env|
      Post.first
      [200, {}, nil]
    }
    body = mw.call({}).last

    assert !ActiveRecord::Base.connection.query_cache.empty?, 'cache not empty'
    body.close
    assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty'
  end

  def test_cache_passing_a_relation
    post = Post.first
    Post.cache do
      query = post.categories.select(:post_id)
      assert Post.connection.select_all(query).is_a?(ActiveRecord::Result)
    end
  end

  def test_find_queries
    assert_queries(2) { Task.find(1); Task.find(1) }
  end

  def test_find_queries_with_cache
    Task.cache do
      assert_queries(1) { Task.find(1); Task.find(1) }
    end
  end

  def test_find_queries_with_cache_multi_record
    Task.cache do
      assert_queries(2) { Task.find(1); Task.find(1); Task.find(2) }
    end
  end

  def test_find_queries_with_multi_cache_blocks
    Task.cache do
      Task.cache do
        assert_queries(2) { Task.find(1); Task.find(2) }
      end
      assert_queries(0) { Task.find(1); Task.find(1); Task.find(2) }
    end
  end

  def test_count_queries_with_cache
    Task.cache do
      assert_queries(1) { Task.count; Task.count }
    end
  end

  def test_query_cache_dups_results_correctly
    Task.cache do
      now  = Time.now.utc
      task = Task.find 1
      assert_not_equal now, task.starting
      task.starting = now
      task.reload
      assert_not_equal now, task.starting
    end
  end

  def test_cache_is_flat
    Task.cache do
      Topic.columns # don't count this query
      assert_queries(1) { Topic.find(1); Topic.find(1); }
    end

    ActiveRecord::Base.cache do
      assert_queries(1) { Task.find(1); Task.find(1) }
    end
  end

  def test_cache_does_not_wrap_string_results_in_arrays
    Task.cache do
      # Oracle adapter returns count() as Fixnum or Float
      if current_adapter?(:OracleAdapter)
        assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
      elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter)
        # Future versions of the sqlite3 adapter will return numeric
        assert_instance_of Fixnum,
         Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
      else
        assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
      end
    end
  end

  def test_cache_is_ignored_for_locked_relations
    task = Task.find 1

    Task.cache do
      assert_queries(2) { task.lock!; task.lock! }
    end
  end

  def test_cache_is_available_when_connection_is_connected
    conf = ActiveRecord::Base.configurations

    ActiveRecord::Base.configurations = {}
    Task.cache do
      assert_queries(1) { Task.find(1); Task.find(1) }
    end
  ensure
    ActiveRecord::Base.configurations = conf
  end

  def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries
    ActiveRecord::Base.connection.enable_query_cache!
    post = Post.first

    Post.transaction do
      post.update_attributes(title: 'rollback')
      assert_equal 1, Post.where(title: 'rollback').to_a.count
      raise ActiveRecord::Rollback
    end

    assert_equal 0, Post.where(title: 'rollback').to_a.count

    ActiveRecord::Base.connection.uncached do
      assert_equal 0, Post.where(title: 'rollback').to_a.count
    end

    begin
      Post.transaction do
        post.update_attributes(title: 'rollback')
        assert_equal 1, Post.where(title: 'rollback').to_a.count
        raise 'broken'
      end
    rescue Exception
    end

    assert_equal 0, Post.where(title: 'rollback').to_a.count

    ActiveRecord::Base.connection.uncached do
      assert_equal 0, Post.where(title: 'rollback').to_a.count
    end
  end
end

class QueryCacheExpiryTest < ActiveRecord::TestCase
  fixtures :tasks, :posts, :categories, :categories_posts

  def test_cache_gets_cleared_after_migration
    # warm the cache
    Post.find(1)

    # change the column definition
    Post.connection.change_column :posts, :title, :string, limit: 80
    assert_nothing_raised { Post.find(1) }

    # restore the old definition
    Post.connection.change_column :posts, :title, :string
  end

  def test_find
    assert_called(Task.connection, :clear_query_cache) do
      assert !Task.connection.query_cache_enabled
      Task.cache do
        assert Task.connection.query_cache_enabled
        Task.find(1)

        Task.uncached do
          assert !Task.connection.query_cache_enabled
          Task.find(1)
        end

        assert Task.connection.query_cache_enabled
      end
      assert !Task.connection.query_cache_enabled
    end
  end

  def test_update
    assert_called(Task.connection, :clear_query_cache, times: 2) do
      Task.cache do
        task = Task.find(1)
        task.starting = Time.now.utc
        task.save!
      end
    end
  end

  def test_destroy
    assert_called(Task.connection, :clear_query_cache, times: 2) do
      Task.cache do
        Task.find(1).destroy
      end
    end
  end

  def test_insert
    assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
      Task.cache do
        Task.create!
      end
    end
  end

  def test_cache_is_expired_by_habtm_update
    assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
      ActiveRecord::Base.cache do
        c = Category.first
        p = Post.first
        p.categories << c
      end
    end
  end

  def test_cache_is_expired_by_habtm_delete
    assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
      ActiveRecord::Base.cache do
        p = Post.find(1)
        assert p.categories.any?
        p.categories.delete_all
      end
    end
  end
end