record-cache 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +8 -8
  2. data/lib/record_cache/base.rb +4 -4
  3. data/lib/record_cache/datastore/active_record_30.rb +1 -1
  4. data/lib/record_cache/datastore/active_record_31.rb +1 -1
  5. data/lib/record_cache/datastore/active_record_32.rb +2 -2
  6. data/lib/record_cache/datastore/active_record_40.rb +445 -0
  7. data/lib/record_cache/datastore/active_record_41.rb +446 -0
  8. data/lib/record_cache/strategy/full_table_cache.rb +1 -1
  9. data/lib/record_cache/strategy/util.rb +20 -3
  10. data/lib/record_cache/version.rb +1 -1
  11. data/spec/db/create-record-cache-db_and_user.sql +5 -0
  12. data/spec/db/database.yml +7 -0
  13. data/spec/db/schema.rb +9 -15
  14. data/spec/initializers/backward_compatibility.rb +32 -0
  15. data/spec/lib/active_record/visitor_spec.rb +1 -1
  16. data/spec/lib/base_spec.rb +2 -2
  17. data/spec/lib/dispatcher_spec.rb +1 -1
  18. data/spec/lib/multi_read_spec.rb +1 -1
  19. data/spec/lib/query_spec.rb +1 -1
  20. data/spec/lib/statistics_spec.rb +1 -1
  21. data/spec/lib/strategy/base_spec.rb +39 -39
  22. data/spec/lib/strategy/full_table_cache_spec.rb +18 -18
  23. data/spec/lib/strategy/index_cache_spec.rb +58 -52
  24. data/spec/lib/strategy/query_cache_spec.rb +1 -1
  25. data/spec/lib/strategy/unique_index_on_id_cache_spec.rb +57 -45
  26. data/spec/lib/strategy/unique_index_on_string_cache_spec.rb +47 -45
  27. data/spec/lib/strategy/util_spec.rb +49 -43
  28. data/spec/lib/version_store_spec.rb +1 -1
  29. data/spec/models/apple.rb +1 -2
  30. data/spec/spec_helper.rb +16 -7
  31. data/spec/support/matchers/hit_cache_matcher.rb +1 -1
  32. data/spec/support/matchers/miss_cache_matcher.rb +1 -1
  33. data/spec/support/matchers/use_cache_matcher.rb +1 -1
  34. metadata +63 -17
  35. data/spec/support/after_commit.rb +0 -73
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NjcyYzNmZjBjM2VjZGU4MzMzMGI1YjBkNDdiNmQ5NWZiODNiZjE3OA==
4
+ YzM5MTA3YWI2MTRjMDIwMGFmN2FhYzQxZmNlMWU2ZTNiNTkzODkyMQ==
5
5
  data.tar.gz: !binary |-
6
- MzU0ZDM3ZjE1YzhmNTY2MTM3M2MwZmU4ZjZhZTI5ZGQ1NWMwNTMwZg==
6
+ MGYzNGU3NzVlZTI4YTMwZTM5NDhkZDVjZGVjY2UyODJlMWU1YzIzMQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NDE4MTQxYjA2ZDQ2ZTE1ZWVlOTk1MjY5OGU3ZDJjNThjZGQ4MjIxZTE1MWQy
10
- MjMyZmViZmUzZWY2MGU5NDUzOWNhOWFiYWY0MTlmYTAzMTJlOTVjZTA2N2Yz
11
- ODgyNDFiMWY5YzU1MzRhYzNjZWM5M2UyMjU1OGFmNzJlMjliMGI=
9
+ MzQxNDM1ZWQxMmUzYmQyNzZhZjZmNDA2MmQzZWViMWYwZDhlMzI0YTczYjY4
10
+ ZmE4ZDZkNjBkMjc3YjUxNGM1YjJhMDlkNzkwZDRlNGM0MTBlMTUzMTBmZThm
11
+ MDg1NWNjMzZhOGE3OWIxMjc0ODg5YzZmYjY1MmY5MWRkNWFhMDc=
12
12
  data.tar.gz: !binary |-
13
- NDBiNGYyZjIwNDlkMzg5MDU0YjhhNjBjNThiMDhjMzk1YzE1YjgxODUwYTJj
14
- ZTk5YzAzNzY1ZmU4MWI2ZjYyYTU1MjQ4NjNkOTEwZmIxMWMzNzAyYWY5NGJj
15
- YTc2ZDVlN2UyOTZhMzBhY2NjMmUwMWQ0NDI5MjliYjY5MGZjM2Y=
13
+ ZDA2OTRjOTZkODFhMTdiNGU3M2IzZDZiYTljNTViZTdjZGQ2ZjRjODNmY2Ew
14
+ MzA4OTgwYmMyNTE2YzRlZDI4Yjg1MjhiMGFlNTdmYzFmNWZlYWZiYmJlZDc0
15
+ YzMzODM2Y2UyNDIzYjQyNzAwMWY0NWIxZWI0ZjBkNDJlNDVkMTQ=
@@ -9,7 +9,7 @@ module RecordCache
9
9
  module Base
10
10
  class << self
11
11
  def included(klass)
12
- klass.class_eval do
12
+ klass.class_eval do
13
13
  extend ClassMethods
14
14
  include InstanceMethods
15
15
  end
@@ -113,7 +113,7 @@ module RecordCache
113
113
  # :store => the cache store for the instances, e.g. :memory_store, :dalli_store* (default: Rails.cache)
114
114
  # or one of the store ids defined using +RecordCache::Base.register_store+
115
115
  # :key => provide a unique shorter key to limit the cache key length (default: model.name)
116
- #
116
+ #
117
117
  # cache strategy specific options:
118
118
  # :index => one or more attributes (Symbols) for which the ids are cached for the value of the attribute
119
119
  #
@@ -124,7 +124,7 @@ module RecordCache
124
124
  # - use :index => :person_id for aggregated has_many associations
125
125
  def cache_records(options = {})
126
126
  unless @rc_dispatcher
127
- @rc_dispatcher = RecordCache::Dispatcher.new(self)
127
+ @rc_dispatcher = RecordCache::Dispatcher.new(self)
128
128
  # Callback for Data Store specific initialization
129
129
  record_cache_init
130
130
 
@@ -174,4 +174,4 @@ module RecordCache
174
174
  end
175
175
 
176
176
  end
177
- end
177
+ end
@@ -173,7 +173,7 @@ module RecordCache
173
173
  @cacheable = false unless o.groups.empty?
174
174
  visit o.froms if @cacheable
175
175
  visit o.wheres if @cacheable
176
- # skip o.projections
176
+ @cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
177
177
  end
178
178
 
179
179
  def visit_Arel_Nodes_SelectStatement o
@@ -199,7 +199,7 @@ module RecordCache
199
199
  visit o.froms if @cacheable
200
200
  visit o.wheres if @cacheable
201
201
  visit o.source if @cacheable
202
- # skip o.projections
202
+ @cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
203
203
  end
204
204
 
205
205
  def visit_Arel_Nodes_SelectStatement o
@@ -33,7 +33,7 @@ module RecordCache
33
33
  arel = sql.is_a?(String) ? sql.instance_variable_get(:@arel) : sql
34
34
 
35
35
  sanitized_sql = sanitize_sql(sql)
36
- sanitized_sql = connection.to_sql(sanitized_sql, binds) if sanitized_sql.respond_to?(:ast)
36
+ sanitized_sql = connection.to_sql(sanitized_sql, binds.dup) if sanitized_sql.respond_to?(:ast)
37
37
 
38
38
  records = if connection.query_cache_enabled
39
39
  query_cache = connection.instance_variable_get(:@query_cache)
@@ -210,7 +210,7 @@ module RecordCache
210
210
  visit o.froms if @cacheable
211
211
  visit o.wheres if @cacheable
212
212
  visit o.source if @cacheable
213
- # skip o.projections
213
+ @cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
214
214
  end
215
215
 
216
216
  def visit_Arel_Nodes_SelectStatement o
@@ -0,0 +1,445 @@
1
+ module RecordCache
2
+ module ActiveRecord
3
+
4
+ module Base
5
+ class << self
6
+ def included(klass)
7
+ klass.extend ClassMethods
8
+ klass.class_eval do
9
+ class << self
10
+ alias_method_chain :find_by_sql, :record_cache
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ # the tests are always run within a transaction, so the threshold is one higher
18
+ RC_TRANSACTIONS_THRESHOLD = ENV['RAILS_ENV'] == 'test' ? 1 : 0
19
+
20
+ # add cache invalidation hooks on initialization
21
+ def record_cache_init
22
+ after_commit :record_cache_create, :on => :create, :prepend => true
23
+ after_commit :record_cache_update, :on => :update, :prepend => true
24
+ after_commit :record_cache_destroy, :on => :destroy, :prepend => true
25
+ end
26
+
27
+ # Retrieve the records, possibly from cache
28
+ def find_by_sql_with_record_cache(sql, binds = [])
29
+ # Shortcut, no caching please
30
+ return find_by_sql_without_record_cache(sql, binds) unless record_cache?
31
+
32
+ # check the piggy-back'd ActiveRelation record to see if the query can be retrieved from cache
33
+ arel = sql.is_a?(String) ? sql.instance_variable_get(:@arel) : sql
34
+
35
+ sanitized_sql = sanitize_sql(sql)
36
+ sanitized_sql = connection.to_sql(sanitized_sql, binds) if sanitized_sql.respond_to?(:ast)
37
+
38
+ records = if connection.query_cache_enabled
39
+ query_cache = connection.instance_variable_get(:@query_cache)
40
+ query_cache["rc/#{sanitized_sql}"][binds] ||= try_record_cache(arel, sql, binds)
41
+
42
+ elsif connection.open_transactions > RC_TRANSACTIONS_THRESHOLD
43
+ find_by_sql_without_record_cache(sql, binds)
44
+
45
+ else
46
+ try_record_cache(arel, sql, binds)
47
+ end
48
+ records
49
+ end
50
+
51
+ def try_record_cache(arel, sql, binds)
52
+ query = arel && arel.respond_to?(:ast) ? RecordCache::Arel::QueryVisitor.new(binds).accept(arel.ast) : nil
53
+ record_cache.fetch(query) do
54
+ find_by_sql_without_record_cache(sql, binds)
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
62
+
63
+ module Arel
64
+
65
+ # The method <ActiveRecord::Base>.find_by_sql is used to actually
66
+ # retrieve the data from the DB.
67
+ # Unfortunately the ActiveRelation record is not accessible from
68
+ # there, so it is piggy-back'd in the SQL string.
69
+ module TreeManager
70
+ def self.included(klass)
71
+ klass.extend ClassMethods
72
+ klass.send(:include, InstanceMethods)
73
+ klass.class_eval do
74
+ alias_method_chain :to_sql, :record_cache
75
+ end
76
+ end
77
+
78
+ module ClassMethods
79
+ end
80
+
81
+ module InstanceMethods
82
+ def to_sql_with_record_cache
83
+ sql = to_sql_without_record_cache
84
+ sql.instance_variable_set(:@arel, self)
85
+ sql
86
+ end
87
+ end
88
+ end
89
+
90
+ # Visitor for the ActiveRelation to extract a simple cache query
91
+ # Only accepts single select queries with equality where statements
92
+ # Rejects queries with grouping / having / offset / etc.
93
+ class QueryVisitor < ::Arel::Visitors::Visitor
94
+ DESC = "DESC".freeze
95
+ COMMA = ",".freeze
96
+
97
+ def initialize(bindings)
98
+ super()
99
+ @bindings = (bindings || []).inject({}){ |h, cv| column, value = cv; h[column.name] = value; h}
100
+ @cacheable = true
101
+ @query = ::RecordCache::Query.new
102
+ end
103
+
104
+ def accept ast
105
+ super
106
+ @cacheable && !ast.lock ? @query : nil
107
+ end
108
+
109
+ private
110
+
111
+ def not_cacheable o, attribute
112
+ @cacheable = false
113
+ end
114
+
115
+ def skip o, attribute
116
+ end
117
+
118
+ alias :visit_Arel_Nodes_TableAlias :not_cacheable
119
+
120
+ alias :visit_Arel_Nodes_Lock :not_cacheable
121
+
122
+ alias :visit_Arel_Nodes_Sum :not_cacheable
123
+ alias :visit_Arel_Nodes_Max :not_cacheable
124
+ alias :visit_Arel_Nodes_Min :not_cacheable
125
+ alias :visit_Arel_Nodes_Avg :not_cacheable
126
+ alias :visit_Arel_Nodes_Count :not_cacheable
127
+ alias :visit_Arel_Nodes_Addition :not_cacheable
128
+ alias :visit_Arel_Nodes_Subtraction :not_cacheable
129
+ alias :visit_Arel_Nodes_Multiplication :not_cacheable
130
+ alias :visit_Arel_Nodes_NamedFunction :not_cacheable
131
+
132
+ alias :visit_Arel_Nodes_Bin :not_cacheable
133
+ alias :visit_Arel_Nodes_Distinct :not_cacheable
134
+ alias :visit_Arel_Nodes_DistinctOn :not_cacheable
135
+ alias :visit_Arel_Nodes_Division :not_cacheable
136
+ alias :visit_Arel_Nodes_Except :not_cacheable
137
+ alias :visit_Arel_Nodes_Exists :not_cacheable
138
+ alias :visit_Arel_Nodes_InfixOperation :not_cacheable
139
+ alias :visit_Arel_Nodes_Intersect :not_cacheable
140
+ alias :visit_Arel_Nodes_Union :not_cacheable
141
+ alias :visit_Arel_Nodes_UnionAll :not_cacheable
142
+ alias :visit_Arel_Nodes_With :not_cacheable
143
+ alias :visit_Arel_Nodes_WithRecursive :not_cacheable
144
+
145
+ def visit_Arel_Nodes_JoinSource o, attribute
146
+ # left and right are array, but using blank as it also works for nil
147
+ @cacheable = o.left.blank? || o.right.blank?
148
+ end
149
+
150
+ alias :visit_Arel_Nodes_CurrentRow :not_cacheable
151
+ alias :visit_Arel_Nodes_Extract :not_cacheable
152
+ alias :visit_Arel_Nodes_Following :not_cacheable
153
+ alias :visit_Arel_Nodes_NamedWindow :not_cacheable
154
+ alias :visit_Arel_Nodes_Over :not_cacheable
155
+ alias :visit_Arel_Nodes_Preceding :not_cacheable
156
+ alias :visit_Arel_Nodes_Range :not_cacheable
157
+ alias :visit_Arel_Nodes_Rows :not_cacheable
158
+ alias :visit_Arel_Nodes_Window :not_cacheable
159
+
160
+ alias :visit_Arel_Nodes_As :skip
161
+ alias :visit_Arel_SelectManager :not_cacheable
162
+ alias :visit_Arel_Nodes_Ascending :skip
163
+ alias :visit_Arel_Nodes_Descending :skip
164
+ alias :visit_Arel_Nodes_False :skip
165
+ alias :visit_Arel_Nodes_True :skip
166
+
167
+ alias :visit_Arel_Nodes_StringJoin :not_cacheable
168
+ alias :visit_Arel_Nodes_InnerJoin :not_cacheable
169
+ alias :visit_Arel_Nodes_OuterJoin :not_cacheable
170
+
171
+ alias :visit_Arel_Nodes_DeleteStatement :not_cacheable
172
+ alias :visit_Arel_Nodes_InsertStatement :not_cacheable
173
+ alias :visit_Arel_Nodes_UpdateStatement :not_cacheable
174
+
175
+
176
+ alias :unary :not_cacheable
177
+ alias :visit_Arel_Nodes_Group :unary
178
+ alias :visit_Arel_Nodes_Having :unary
179
+ alias :visit_Arel_Nodes_Not :unary
180
+ alias :visit_Arel_Nodes_On :unary
181
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
182
+
183
+ def visit_Arel_Nodes_Offset o, attribute
184
+ @cacheable = false unless o.expr == 0
185
+ end
186
+
187
+ def visit_Arel_Nodes_Values o, attribute
188
+ visit o.expressions if @cacheable
189
+ end
190
+
191
+ def visit_Arel_Nodes_Limit o, attribute
192
+ @query.limit = o.expr
193
+ end
194
+ alias :visit_Arel_Nodes_Top :visit_Arel_Nodes_Limit
195
+
196
+ GROUPING_EQUALS_REGEXP = /^\W?(\w*)\W?\.\W?(\w*)\W?\s*=\s*(\d+)$/ # `calendars`.account_id = 5
197
+ GROUPING_IN_REGEXP = /^^\W?(\w*)\W?\.\W?(\w*)\W?\s*IN\s*\(([\d\s,]+)\)$/ # `service_instances`.`id` IN (118,80,120,82)
198
+ def visit_Arel_Nodes_Grouping o, attribute
199
+ return unless @cacheable
200
+ if @table_name && o.expr =~ GROUPING_EQUALS_REGEXP && $1 == @table_name
201
+ @cacheable = @query.where($2, $3.to_i)
202
+ elsif @table_name && o.expr =~ GROUPING_IN_REGEXP && $1 == @table_name
203
+ @cacheable = @query.where($2, $3.split(',').map(&:to_i))
204
+ else
205
+ @cacheable = false
206
+ end
207
+ end
208
+
209
+ def visit_Arel_Nodes_SelectCore o, attribute
210
+ @cacheable = false unless o.groups.empty?
211
+ visit o.froms if @cacheable
212
+ visit o.wheres if @cacheable
213
+ visit o.source if @cacheable
214
+ @cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
215
+ end
216
+
217
+ def visit_Arel_Nodes_SelectStatement o, attribute
218
+ @cacheable = false if o.cores.size > 1
219
+ if @cacheable
220
+ visit o.offset
221
+ o.orders.map { |x| handle_order_by(visit x) } if @cacheable && o.orders.size > 0
222
+ visit o.limit
223
+ visit o.cores
224
+ end
225
+ end
226
+
227
+ ORDER_BY_REGEXP = /^\s*([\w\.]*)\s*(|ASC|asc|DESC|desc)\s*$/ # people.id DESC
228
+ def handle_order_by(order)
229
+ order.to_s.split(COMMA).each do |o|
230
+ # simple sort order (+people.id+ can be replaced by +id+, as joins are not allowed anyways)
231
+ if o.match(ORDER_BY_REGEXP)
232
+ asc = $2.upcase == DESC ? false : true
233
+ @query.order_by($1.split('.').last, asc)
234
+ else
235
+ @cacheable = false
236
+ end
237
+ end
238
+ end
239
+
240
+ def visit_Arel_Table o, attribute
241
+ @table_name = o.name
242
+ end
243
+
244
+ def visit_Arel_Attributes_Attribute o, attribute
245
+ o.name.to_sym
246
+ end
247
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
248
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
249
+ alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
250
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
251
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
252
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
253
+
254
+ def visit_Arel_Nodes_Equality o, attribute
255
+ key, value = visit(o.left), visit(o.right)
256
+ # several different binding markers exist depending on the db driver used (MySQL, Postgress supported)
257
+ if value.to_s =~ /^(\?|\u0000|\$\d+)$/
258
+ # puts "bindings: #{@bindings.inspect}, key = #{key.to_s}"
259
+ value = @bindings[key.to_s] || value
260
+ end
261
+ # puts " =====> equality found: #{key.inspect}@#{key.class.name} => #{value.inspect}@#{value.class.name}"
262
+ @query.where(key, value)
263
+ end
264
+ alias :visit_Arel_Nodes_In :visit_Arel_Nodes_Equality
265
+
266
+ def visit_Arel_Nodes_And o, attribute
267
+ visit(o.children)
268
+ end
269
+
270
+ alias :visit_Arel_Nodes_Or :not_cacheable
271
+ alias :visit_Arel_Nodes_NotEqual :not_cacheable
272
+ alias :visit_Arel_Nodes_GreaterThan :not_cacheable
273
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :not_cacheable
274
+ alias :visit_Arel_Nodes_Assignment :not_cacheable
275
+ alias :visit_Arel_Nodes_LessThan :not_cacheable
276
+ alias :visit_Arel_Nodes_LessThanOrEqual :not_cacheable
277
+ alias :visit_Arel_Nodes_Between :not_cacheable
278
+ alias :visit_Arel_Nodes_NotIn :not_cacheable
279
+ alias :visit_Arel_Nodes_DoesNotMatch :not_cacheable
280
+ alias :visit_Arel_Nodes_Matches :not_cacheable
281
+
282
+ def visit_Fixnum o, attribute
283
+ o.to_i
284
+ end
285
+ alias :visit_Bignum :visit_Fixnum
286
+
287
+ def visit_Symbol o, attribute
288
+ o.to_sym
289
+ end
290
+
291
+ def visit_Object o, attribute
292
+ o
293
+ end
294
+ alias :visit_Arel_Nodes_SqlLiteral :visit_Object
295
+ alias :visit_Arel_Nodes_BindParam :visit_Object
296
+ alias :visit_Arel_SqlLiteral :visit_Object # This is deprecated
297
+ alias :visit_String :visit_Object
298
+ alias :visit_NilClass :visit_Object
299
+ alias :visit_TrueClass :visit_Object
300
+ alias :visit_FalseClass :visit_Object
301
+ alias :visit_Arel_SqlLiteral :visit_Object
302
+ alias :visit_BigDecimal :visit_Object
303
+ alias :visit_Float :visit_Object
304
+ alias :visit_Time :visit_Object
305
+ alias :visit_Date :visit_Object
306
+ alias :visit_DateTime :visit_Object
307
+ alias :visit_Hash :visit_Object
308
+
309
+ def visit_Array o, attribute
310
+ o.map{ |x| visit x }
311
+ end
312
+ end
313
+ end
314
+
315
+ end
316
+
317
+ module RecordCache
318
+
319
+ # Patch ActiveRecord::Relation to make sure update_all will invalidate all referenced records
320
+ module ActiveRecord
321
+ module UpdateAll
322
+ class << self
323
+ def included(klass)
324
+ klass.extend ClassMethods
325
+ klass.send(:include, InstanceMethods)
326
+ klass.class_eval do
327
+ alias_method_chain :update_all, :record_cache
328
+ end
329
+ end
330
+ end
331
+
332
+ module ClassMethods
333
+ end
334
+
335
+ module InstanceMethods
336
+ def __find_in_clause(sub_select)
337
+ return nil unless sub_select.arel.constraints.count == 1
338
+ constraint = sub_select.arel.constraints.first
339
+ return constraint if constraint.is_a?(::Arel::Nodes::In) # directly an IN clause
340
+ return nil unless constraint.respond_to?(:children) && constraint.children.count == 1
341
+ constraint = constraint.children.first
342
+ return constraint if constraint.is_a?(::Arel::Nodes::In) # AND with IN clause
343
+ nil
344
+ end
345
+
346
+ def update_all_with_record_cache(updates, conditions = nil, options = {})
347
+ result = update_all_without_record_cache(updates, conditions, options)
348
+
349
+ if record_cache?
350
+ # when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
351
+ unless conditions || options.present? || @limit_value.present? != @order_values.present?
352
+ # get all attributes that contain a unique index for this model
353
+ unique_index_attributes = RecordCache::Strategy::UniqueIndexCache.attributes(self)
354
+ # go straight to SQL result (without instantiating records) for optimal performance
355
+ RecordCache::Base.version_store.multi do
356
+ sub_select = select(unique_index_attributes.map(&:to_s).join(','))
357
+ in_clause = __find_in_clause(sub_select)
358
+ if unique_index_attributes.size == 1 && in_clause &&
359
+ in_clause.left.try(:name).to_s == unique_index_attributes.first.to_s
360
+ # common case where the unique index is the (only) constraint on the query: SELECT id FROM people WHERE id in (...)
361
+ attribute = unique_index_attributes.first
362
+ in_clause.right.each do |value|
363
+ record_cache.invalidate(attribute, value)
364
+ end
365
+ else
366
+ connection.execute(sub_select.to_sql).each do |row|
367
+ # invalidate the unique index for all attributes
368
+ unique_index_attributes.each_with_index do |attribute, index|
369
+ record_cache.invalidate(attribute, (row.is_a?(Hash) ? row[attribute.to_s] : row[index]))
370
+ end
371
+ end
372
+ end
373
+ end
374
+ end
375
+ end
376
+
377
+ result
378
+ end
379
+ end
380
+ end
381
+ end
382
+
383
+ # Patch ActiveRecord::Associations::HasManyAssociation to make sure the index_cache is updated when records are
384
+ # deleted from the collection
385
+ module ActiveRecord
386
+ module HasMany
387
+ class << self
388
+ def included(klass)
389
+ klass.extend ClassMethods
390
+ klass.send(:include, InstanceMethods)
391
+ klass.class_eval do
392
+ alias_method_chain :delete_records, :record_cache
393
+ end
394
+ end
395
+ end
396
+
397
+ module ClassMethods
398
+ end
399
+
400
+ module InstanceMethods
401
+ def delete_records_with_record_cache(records, method)
402
+ # invalidate :id cache for all records
403
+ records.each{ |record| record.class.record_cache.invalidate(record.id) if record.class.record_cache? unless record.new_record? }
404
+ # invalidate the referenced class for the attribute/value pair on the index cache
405
+ @reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
406
+ delete_records_without_record_cache(records, method)
407
+ end
408
+ end
409
+ end
410
+
411
+ module HasOne
412
+ class << self
413
+ def included(klass)
414
+ klass.extend ClassMethods
415
+ klass.send(:include, InstanceMethods)
416
+ klass.class_eval do
417
+ alias_method_chain :delete, :record_cache
418
+ end
419
+ end
420
+ end
421
+
422
+ module ClassMethods
423
+ end
424
+
425
+ module InstanceMethods
426
+ def delete_with_record_cache(method = options[:dependent])
427
+ # invalidate :id cache for all record
428
+ if load_target
429
+ target.class.record_cache.invalidate(target.id) if target.class.record_cache? unless target.new_record?
430
+ end
431
+ # invalidate the referenced class for the attribute/value pair on the index cache
432
+ @reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
433
+ delete_without_record_cache(method)
434
+ end
435
+ end
436
+ end
437
+ end
438
+
439
+ end
440
+
441
+ ActiveRecord::Base.send(:include, RecordCache::ActiveRecord::Base)
442
+ Arel::TreeManager.send(:include, RecordCache::Arel::TreeManager)
443
+ ActiveRecord::Relation.send(:include, RecordCache::ActiveRecord::UpdateAll)
444
+ ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
445
+ ActiveRecord::Associations::HasOneAssociation.send(:include, RecordCache::ActiveRecord::HasOne)