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
@@ -0,0 +1,446 @@
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)
347
+ result = update_all_without_record_cache(updates)
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 @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
+ records = load_target if records == :all
403
+ # invalidate :id cache for all records
404
+ records.each{ |record| record.class.record_cache.invalidate(record.id) if record.class.record_cache? unless record.new_record? }
405
+ # invalidate the referenced class for the attribute/value pair on the index cache
406
+ @reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
407
+ delete_records_without_record_cache(records, method)
408
+ end
409
+ end
410
+ end
411
+
412
+ module HasOne
413
+ class << self
414
+ def included(klass)
415
+ klass.extend ClassMethods
416
+ klass.send(:include, InstanceMethods)
417
+ klass.class_eval do
418
+ alias_method_chain :delete, :record_cache
419
+ end
420
+ end
421
+ end
422
+
423
+ module ClassMethods
424
+ end
425
+
426
+ module InstanceMethods
427
+ def delete_with_record_cache(method = options[:dependent])
428
+ # invalidate :id cache for all record
429
+ if load_target
430
+ target.class.record_cache.invalidate(target.id) if target.class.record_cache? unless target.new_record?
431
+ end
432
+ # invalidate the referenced class for the attribute/value pair on the index cache
433
+ @reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
434
+ delete_without_record_cache(method)
435
+ end
436
+ end
437
+ end
438
+ end
439
+
440
+ end
441
+
442
+ ActiveRecord::Base.send(:include, RecordCache::ActiveRecord::Base)
443
+ Arel::TreeManager.send(:include, RecordCache::Arel::TreeManager)
444
+ ActiveRecord::Relation.send(:include, RecordCache::ActiveRecord::UpdateAll)
445
+ ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
446
+ ActiveRecord::Associations::HasOneAssociation.send(:include, RecordCache::ActiveRecord::HasOne)
@@ -7,7 +7,7 @@ module RecordCache
7
7
  def self.parse(base, record_store, options)
8
8
  return nil unless options[:full_table]
9
9
  return nil unless base.table_exists?
10
-
10
+
11
11
  FullTableCache.new(base, :full_table, record_store, options)
12
12
  end
13
13