redis-memo 0.0.0.beta → 0.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1316982523363ce87b0ada30b82b8200559a07b2f1a6bea54168b40b2afd397e
4
- data.tar.gz: 1278e1ff6b7cc2275416c7af9b8668a13128c105a4cde74640deccd0a4380373
3
+ metadata.gz: 3e64eaaecf192519453c1accb0d10ca25ed2cf8274c10b6cb128a14e9a2e616e
4
+ data.tar.gz: fd98fce04f77660b8fc5960e491695504ef438394a882fb80d36b706038d10e5
5
5
  SHA512:
6
- metadata.gz: 121069c8de985a8977234776ad15fd209c1c0db523c361a5caf9181ed7120533350af16c354d7f76dfc03168230248e37c64b37eb8ce9f6af16cfdc9160448f3
7
- data.tar.gz: 133d18912a95eb599554b9cacb5c5932471772930165249381baaa8aeb403a3a6b21c6e17711a11eda56a1fa6dbf347396c7fe513cec3b53ec6f0c592d5b7d9a
6
+ metadata.gz: 20ca8f3efa4ca8591d073960c5fd5b9c8fbcd920b58ae6fe378ec3511af082c2cf964c72cda93739b6bef7294d5be9ceb77eb6000cb3df567f18b2bd581cf4c8
7
+ data.tar.gz: 1c485024e2eb3f541139bbd8f136279f5b1010b7b81ab30929d2e87a8917c4f0d8fb35d2ae39d94fd14d2f918b6476f53d589026f4412a8b118d6b2b6d732a93
data/lib/redis_memo.rb CHANGED
@@ -5,7 +5,7 @@ require 'json'
5
5
 
6
6
  module RedisMemo
7
7
  require 'redis_memo/memoize_method'
8
- require 'redis_memo/memoize_records'
8
+ require 'redis_memo/memoize_query'
9
9
 
10
10
  DefaultOptions = RedisMemo::Options.new
11
11
 
@@ -16,7 +16,7 @@ class RedisMemo::Memoizable::Dependency
16
16
  if !memo_or_model.is_a?(RedisMemo::Memoizable)
17
17
  [
18
18
  memo_or_model.redis_memo_class_memoizable,
19
- RedisMemo::MemoizeRecords.create_memo(memo_or_model, **conditions),
19
+ RedisMemo::MemoizeQuery.create_memo(memo_or_model, **conditions),
20
20
  ].each do |memo|
21
21
  nodes[memo.cache_key] = memo
22
22
  end
@@ -6,7 +6,7 @@ require_relative 'middleware'
6
6
  require_relative 'options'
7
7
 
8
8
  module RedisMemo::MemoizeMethod
9
- def memoize_method(method_name, **options, &depends_on)
9
+ def memoize_method(method_name, method_id: nil, **options, &depends_on)
10
10
  method_name_without_memo = :"_redis_memo_#{method_name}_without_memo"
11
11
  method_name_with_memo = :"_redis_memo_#{method_name}_with_memo"
12
12
 
@@ -17,7 +17,14 @@ module RedisMemo::MemoizeMethod
17
17
 
18
18
  future = RedisMemo::Future.new(
19
19
  self,
20
- RedisMemo::MemoizeMethod.method_id(self, method_name),
20
+ case method_id
21
+ when NilClass
22
+ RedisMemo::MemoizeMethod.method_id(self, method_name)
23
+ when String, Symbol
24
+ method_id
25
+ else
26
+ method_id.call(self, *args)
27
+ end,
21
28
  args,
22
29
  depends_on,
23
30
  options,
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'memoize_method'
3
+
4
+ if defined?(ActiveRecord)
5
+ # Hook into ActiveRecord to cache SQL queries and perform auto cache
6
+ # invalidation
7
+ module RedisMemo::MemoizeQuery
8
+ require_relative 'memoize_query/cached_select'
9
+ require_relative 'memoize_query/invalidation'
10
+ require_relative 'memoize_query/model_callback'
11
+
12
+ # Only editable columns will be used to create memos that are invalidatable
13
+ # after each record save
14
+ def memoize_table_column(*raw_columns, editable: true)
15
+ RedisMemo::MemoizeQuery.using_active_record!(self)
16
+ columns = raw_columns.map(&:to_sym).sort
17
+
18
+ RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: true) << columns if editable
19
+ RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: false) << columns
20
+
21
+ RedisMemo::MemoizeQuery::ModelCallback.install(self)
22
+ RedisMemo::MemoizeQuery::Invalidation.install(self)
23
+
24
+ if ENV['REDIS_MEMO_DISABLE_CACHED_SELECT'] != 'true'
25
+ RedisMemo::MemoizeQuery::CachedSelect.install(ActiveRecord::Base.connection)
26
+ end
27
+
28
+ # The code below might fail due to missing DB/table errors
29
+ columns.each do |column|
30
+ unless self.columns_hash.include?(column.to_s)
31
+ raise(
32
+ RedisMemo::ArgumentError,
33
+ "'#{self.name}' does not contain column '#{column}'",
34
+ )
35
+ end
36
+ end
37
+
38
+ unless ENV["REDIS_MEMO_DISABLE_QUERY_#{self.table_name.upcase}"] == 'true'
39
+ RedisMemo::MemoizeQuery::CachedSelect.enabled_models[self.table_name] = self
40
+ end
41
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
42
+ # no-opts: models with memoize_table_column decleared might be loaded in
43
+ # rake tasks that are used to create databases
44
+ end
45
+
46
+ def self.using_active_record!(model_class)
47
+ unless model_class.respond_to?(:<) && model_class < ActiveRecord::Base
48
+ raise RedisMemo::ArgumentError, "'#{model_class.name}' does not use ActiveRecord"
49
+ end
50
+ end
51
+
52
+ @@memoized_columns = Hash.new { |h, k| h[k] = [Set.new, Set.new] }
53
+
54
+ def self.memoized_columns(model_or_table, editable_only: false)
55
+ table = model_or_table.is_a?(Class) ? model_or_table.table_name : model_or_table
56
+ @@memoized_columns[table.to_sym][editable_only ? 1 : 0]
57
+ end
58
+
59
+ # extra_props are considered as AND conditions on the model class
60
+ def self.create_memo(model_class, **extra_props)
61
+ using_active_record!(model_class)
62
+
63
+ keys = extra_props.keys.sort
64
+ if !keys.empty? && !memoized_columns(model_class).include?(keys)
65
+ raise(
66
+ RedisMemo::ArgumentError,
67
+ "'#{model_class.name}' has not memoized columns: #{keys}",
68
+ )
69
+ end
70
+
71
+ extra_props.each do |key, values|
72
+ # The data type is ensured by the database, thus we don't need to cast
73
+ # types here for better performance
74
+ column_name = key.to_s
75
+ values = [values] unless values.is_a?(Enumerable)
76
+ extra_props[key] =
77
+ if model_class.defined_enums.include?(column_name)
78
+ enum_mapping = model_class.defined_enums[column_name]
79
+ values.map do |value|
80
+ # Assume a value is a converted enum if it does not exist in the
81
+ # enum mapping
82
+ (enum_mapping[value.to_s] || value).to_s
83
+ end
84
+ else
85
+ values.map(&:to_s)
86
+ end
87
+ end
88
+
89
+ RedisMemo::Memoizable.new(
90
+ __redis_memo_memoize_query_model_class_name__: model_class.name,
91
+ **extra_props,
92
+ )
93
+ end
94
+
95
+ def self.invalidate_all(model_class)
96
+ RedisMemo::Tracer.trace(
97
+ 'redis_memo.memoizable.invalidate_all',
98
+ model_class.name,
99
+ ) do
100
+ RedisMemo::Memoizable.invalidate([model_class.redis_memo_class_memoizable])
101
+ end
102
+ end
103
+
104
+ def self.invalidate(record)
105
+ # Invalidate memos with current values
106
+ memos_to_invalidate = memoized_columns(record.class).map do |columns|
107
+ props = {}
108
+ columns.each do |column|
109
+ props[column] = record.send(column)
110
+ end
111
+
112
+ create_memo(record.class, **props)
113
+ end
114
+
115
+ # Create memos with previous values if
116
+ # - there are saved changes
117
+ # - this is not creating a new record
118
+ if !record.saved_changes.empty? && !record.saved_changes.include?(record.class.primary_key)
119
+ previous_values = {}
120
+ record.saved_changes.each do |column, (previous_value, _)|
121
+ previous_values[column.to_sym] = previous_value
122
+ end
123
+
124
+ memoized_columns(record.class, editable_only: true).each do |columns|
125
+ props = previous_values.slice(*columns)
126
+ next if props.empty?
127
+
128
+ # Fill the column values that have not changed
129
+ columns.each do |column|
130
+ next if props.include?(column)
131
+
132
+ props[column] = record.send(column)
133
+ end
134
+
135
+ memos_to_invalidate << create_memo(record.class, **props)
136
+ end
137
+ end
138
+
139
+ RedisMemo::Memoizable.invalidate(memos_to_invalidate)
140
+ end
141
+ end
142
+ end
@@ -38,7 +38,7 @@
38
38
  # the `memoize_table_column` declaration on the model class.
39
39
  #
40
40
  # class MyRecord < ApplicationRecord
41
- # extend RedisMemo::MemoizeRecords
41
+ # extend RedisMemo::MemoizeQuery
42
42
  # memoize_table_column :value
43
43
  # end
44
44
  #
@@ -87,8 +87,17 @@
87
87
  #
88
88
  # See +extract_bind_params+ for the precise detection logic.
89
89
  #
90
- class RedisMemo::MemoizeRecords::CachedSelect
91
- # TODO: merge this into RedisMemo::MemoizeQuery
90
+ class RedisMemo::MemoizeQuery::CachedSelect
91
+ require_relative 'cached_select/bind_params'
92
+ require_relative 'cached_select/connection_adapter'
93
+ require_relative 'cached_select/statement_cache'
94
+
95
+ @@enabled_models = {}
96
+
97
+ def self.enabled_models
98
+ @@enabled_models
99
+ end
100
+
92
101
  def self.install(connection)
93
102
  klass = connection.class
94
103
  return if klass.singleton_class < RedisMemo::MemoizeMethod
@@ -96,8 +105,14 @@ class RedisMemo::MemoizeRecords::CachedSelect
96
105
  klass.class_eval do
97
106
  extend RedisMemo::MemoizeMethod
98
107
 
99
- memoize_method :exec_query do |_, sql, name, binds, **kwargs|
100
- RedisMemo::MemoizeRecords::CachedSelect
108
+ memoize_method(
109
+ :exec_query,
110
+ method_id: proc do |_, sql, *args|
111
+ sql.gsub(/(\$\d+)/, '?') # $1 -> ?
112
+ .gsub(/((, *)*\?)+/, '?') # (?, ?, ? ...) -> (?)
113
+ end,
114
+ ) do |_, sql, name, binds, **kwargs|
115
+ RedisMemo::MemoizeQuery::CachedSelect
101
116
  .current_query_bind_params
102
117
  .params
103
118
  .each do |model, attrs_set|
@@ -107,8 +122,8 @@ class RedisMemo::MemoizeRecords::CachedSelect
107
122
  end
108
123
 
109
124
  depends_on RedisMemo::Memoizable.new(
110
- __redis_memo_memoize_records_memoize_query_sql__: sql,
111
- __redis_memo_memoize_records_memoize_query_binds__: binds.map(&:value_for_database),
125
+ __redis_memo_memoize_query_memoize_query_sql__: sql,
126
+ __redis_memo_memoize_query_memoize_query_binds__: binds.map(&:value_for_database),
112
127
  )
113
128
  end
114
129
  end
@@ -137,64 +152,6 @@ class RedisMemo::MemoizeRecords::CachedSelect
137
152
  end
138
153
  end
139
154
 
140
- module ConnectionAdapter
141
- def cacheable_query(*args)
142
- query, binds = super(*args)
143
-
144
- # Persist the arel object to StatementCache#execute
145
- query.instance_variable_set(:@__redis_memo_memoize_records_memoize_query_arel, args.last)
146
-
147
- [query, binds]
148
- end
149
-
150
- def exec_query(*args)
151
- # An Arel AST in Thread local is set prior to supported query methods
152
- if !RedisMemo.without_memo? &&
153
- RedisMemo::MemoizeRecords::CachedSelect.extract_bind_params(args[0])
154
- # [Reids $model Load] $sql $binds
155
- RedisMemo::DefaultOptions.logger&.info(
156
- "[Redis] \u001b[36;1m#{args[1]} \u001b[34;1m#{args[0]}\u001b[0m #{
157
- args[2].map { |bind| [bind.name, bind.value_for_database]}
158
- }"
159
- )
160
-
161
- RedisMemo::Tracer.trace(
162
- 'redis_memo.memoize_query',
163
- args[0]
164
- .gsub(/(\$\d+)/, '?') # $1 -> ?
165
- .gsub(/((, *)*\?)+/, '?'), # (?, ?, ? ...) -> (?)
166
- ) do
167
- super(*args)
168
- end
169
- else
170
- RedisMemo.without_memo { super(*args) }
171
- end
172
- end
173
-
174
- def select_all(*args)
175
- if args[0].is_a?(Arel::SelectManager)
176
- RedisMemo::MemoizeRecords::CachedSelect.current_query = args[0]
177
- end
178
-
179
- super(*args)
180
- ensure
181
- RedisMemo::MemoizeRecords::CachedSelect.reset_current_query
182
- end
183
- end
184
-
185
- module StatementCache
186
- def execute(*args)
187
- arel = query_builder.instance_variable_get(:@__redis_memo_memoize_records_memoize_query_arel)
188
- RedisMemo::MemoizeRecords::CachedSelect.current_query = arel
189
- RedisMemo::MemoizeRecords::CachedSelect.current_substitutes =
190
- bind_map.map_substitutes(args[0])
191
-
192
- super(*args)
193
- ensure
194
- RedisMemo::MemoizeRecords::CachedSelect.reset_current_query
195
- end
196
- end
197
-
198
155
  def self.extract_bind_params(sql)
199
156
  ast = Thread.current[THREAD_KEY_AREL]&.ast
200
157
  return false unless ast.is_a?(Arel::Nodes::SelectStatement)
@@ -255,16 +212,8 @@ class RedisMemo::MemoizeRecords::CachedSelect
255
212
  return
256
213
  end
257
214
 
258
- type_caster = table_node.send(:type_caster)
259
- binding_relation =
260
- case type_caster
261
- when ActiveRecord::TypeCaster::Map
262
- type_caster.send(:types)
263
- when ActiveRecord::TypeCaster::Connection
264
- type_caster.instance_variable_get(:@klass)
265
- else
266
- return
267
- end
215
+ binding_relation = extract_binding_relation(table_node)
216
+ return unless binding_relation
268
217
 
269
218
  rights = node.right.is_a?(Array) ? node.right : [node.right]
270
219
  substitutes = Thread.current[THREAD_KEY_SUBSTITUTES]
@@ -303,11 +252,17 @@ class RedisMemo::MemoizeRecords::CachedSelect
303
252
  return unless node.orders.empty?
304
253
 
305
254
  node.cores.each do |core|
255
+ # We don't support JOINs
256
+ return unless core.source.right.empty?
257
+
306
258
  # Should have a WHERE if directly selecting from a table
307
259
  source_node = core.source.left
260
+ binding_relation = nil
308
261
  case source_node
309
262
  when Arel::Table
310
- return if core.wheres.empty?
263
+ binding_relation = extract_binding_relation(source_node)
264
+
265
+ return if core.wheres.empty? || binding_relation.nil?
311
266
  when Arel::Nodes::TableAlias
312
267
  bind_params = bind_params.union(
313
268
  extract_bind_params_recurse(source_node.left)
@@ -334,6 +289,9 @@ class RedisMemo::MemoizeRecords::CachedSelect
334
289
 
335
290
  return unless bind_params
336
291
  end
292
+
293
+ # Reject any unbound select queries
294
+ return if binding_relation && bind_params.params[binding_relation].empty?
337
295
  end
338
296
 
339
297
  bind_params
@@ -352,7 +310,7 @@ class RedisMemo::MemoizeRecords::CachedSelect
352
310
  end
353
311
 
354
312
  bind_params
355
- when Arel::Nodes::Join, Arel::Nodes::Union, Arel::Nodes::Or
313
+ when Arel::Nodes::Union, Arel::Nodes::Or
356
314
  [node.left, node.right].each do |child|
357
315
  bind_params = bind_params.union(
358
316
  extract_bind_params_recurse(child)
@@ -368,132 +326,12 @@ class RedisMemo::MemoizeRecords::CachedSelect
368
326
  end
369
327
  end
370
328
 
371
- class BindParams
372
- def params
373
- #
374
- # Bind params is hash of sets: each key is a model class, each value is a
375
- # set of hashes for memoized column conditions. Example:
376
- #
377
- # {
378
- # Site => [
379
- # {name: 'a', city: 'b'},
380
- # {name: 'a', city: 'c'},
381
- # {name: 'b', city: 'b'},
382
- # {name: 'b', city: 'c'},
383
- # ],
384
- # }
385
- #
386
- @params ||= Hash.new do |models, model|
387
- models[model] = []
388
- end
389
- end
390
-
391
- def union(other)
392
- return unless other
393
-
394
- # The tree is almost always right-heavy. Merge into the right node for better
395
- # performance.
396
- other.params.merge!(params) do |_, other_attrs_set, attrs_set|
397
- if other_attrs_set.empty?
398
- attrs_set
399
- elsif attrs_set.empty?
400
- other_attrs_set
401
- else
402
- attrs_set + other_attrs_set
403
- end
404
- end
405
-
406
- other
407
- end
408
-
409
- def product(other)
410
- # Example:
411
- #
412
- # and(
413
- # [{a: 1}, {a: 2}],
414
- # [{b: 1}, {b: 2}],
415
- # )
416
- #
417
- # =>
418
- #
419
- # [
420
- # {a: 1, b: 1},
421
- # {a: 1, b: 2},
422
- # {a: 2, b: 1},
423
- # {a: 2, b: 2},
424
- # ]
425
- return unless other
426
-
427
- # The tree is almost always right-heavy. Merge into the right node for better
428
- # performance.
429
- params.each do |model, attrs_set|
430
- next if attrs_set.empty?
431
-
432
- # The other model does not have any conditions so far: carry the
433
- # attributes over to the other node
434
- if other.params[model].empty?
435
- other.params[model] = attrs_set
436
- next
437
- end
438
-
439
- # Distribute the current attrs into the other
440
- other_attrs_set_size = other.params[model].size
441
- other_attrs_set = other.params[model]
442
- merged_attrs_set = Array.new(other_attrs_set_size * attrs_set.size)
443
-
444
- attrs_set.each_with_index do |attrs, i|
445
- other_attrs_set.each_with_index do |other_attrs, j|
446
- k = i * other_attrs_set_size + j
447
- merged_attrs = merged_attrs_set[k] = other_attrs.dup
448
- attrs.each do |name, val|
449
- # Conflict detected. For example:
450
- #
451
- # (a = 1 or b = 1) and (a = 2 or b = 2)
452
- #
453
- # Keep: a = 1 and b = 2, a = 2 and b = 1
454
- # Discard: a = 1 and a = 2, b = 1 and b = 2
455
- if merged_attrs.include?(name) && merged_attrs[name] != val
456
- merged_attrs_set[k] = nil
457
- break
458
- end
459
-
460
- merged_attrs[name] = val
461
- end
462
- end
463
- end
464
-
465
- merged_attrs_set.compact!
466
- other.params[model] = merged_attrs_set
467
- end
468
-
469
- other
470
- end
471
-
472
- def uniq!
473
- params.each do |_, attrs_set|
474
- attrs_set.uniq!
475
- end
476
- end
477
-
478
- def memoizable?
479
- return false if params.empty?
480
-
481
- params.each do |model, attrs_set|
482
- return false if attrs_set.empty?
483
-
484
- attrs_set.each do |attrs|
485
- return false unless RedisMemo::MemoizeRecords
486
- .memoized_columns(model)
487
- .include?(attrs.keys.sort)
488
- end
489
- end
490
-
491
- true
492
- end
329
+ def self.extract_binding_relation(table_node)
330
+ enabled_models[table_node.try(:name)]
493
331
  end
494
332
 
495
333
  # Thread locals to exchange information between RedisMemo and ActiveRecord
496
- THREAD_KEY_AREL = :__redis_memo_memoize_records_cached_select_arel__
497
- THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_records_cached_select_substitues__
498
- THREAD_KEY_AREL_BIND_PARAMS = :__redis_memo_memoize_records_cached_select_arel_bind_params__
334
+ THREAD_KEY_AREL = :__redis_memo_memoize_query_cached_select_arel__
335
+ THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_query_cached_select_substitues__
336
+ THREAD_KEY_AREL_BIND_PARAMS = :__redis_memo_memoize_query_cached_select_arel_bind_params__
499
337
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisMemo::MemoizeQuery::CachedSelect
4
+ class BindParams
5
+ def params
6
+ #
7
+ # Bind params is hash of sets: each key is a model class, each value is a
8
+ # set of hashes for memoized column conditions. Example:
9
+ #
10
+ # {
11
+ # Site => [
12
+ # {name: 'a', city: 'b'},
13
+ # {name: 'a', city: 'c'},
14
+ # {name: 'b', city: 'b'},
15
+ # {name: 'b', city: 'c'},
16
+ # ],
17
+ # }
18
+ #
19
+ @params ||= Hash.new do |models, model|
20
+ models[model] = []
21
+ end
22
+ end
23
+
24
+ def union(other)
25
+ return unless other
26
+
27
+ # The tree is almost always right-heavy. Merge into the right node for better
28
+ # performance.
29
+ other.params.merge!(params) do |_, other_attrs_set, attrs_set|
30
+ if other_attrs_set.empty?
31
+ attrs_set
32
+ elsif attrs_set.empty?
33
+ other_attrs_set
34
+ else
35
+ attrs_set + other_attrs_set
36
+ end
37
+ end
38
+
39
+ other
40
+ end
41
+
42
+ def product(other)
43
+ # Example:
44
+ #
45
+ # and(
46
+ # [{a: 1}, {a: 2}],
47
+ # [{b: 1}, {b: 2}],
48
+ # )
49
+ #
50
+ # =>
51
+ #
52
+ # [
53
+ # {a: 1, b: 1},
54
+ # {a: 1, b: 2},
55
+ # {a: 2, b: 1},
56
+ # {a: 2, b: 2},
57
+ # ]
58
+ return unless other
59
+
60
+ # The tree is almost always right-heavy. Merge into the right node for better
61
+ # performance.
62
+ params.each do |model, attrs_set|
63
+ next if attrs_set.empty?
64
+
65
+ # The other model does not have any conditions so far: carry the
66
+ # attributes over to the other node
67
+ if other.params[model].empty?
68
+ other.params[model] = attrs_set
69
+ next
70
+ end
71
+
72
+ # Distribute the current attrs into the other
73
+ other_attrs_set_size = other.params[model].size
74
+ other_attrs_set = other.params[model]
75
+ merged_attrs_set = Array.new(other_attrs_set_size * attrs_set.size)
76
+
77
+ attrs_set.each_with_index do |attrs, i|
78
+ other_attrs_set.each_with_index do |other_attrs, j|
79
+ k = i * other_attrs_set_size + j
80
+ merged_attrs = merged_attrs_set[k] = other_attrs.dup
81
+ attrs.each do |name, val|
82
+ # Conflict detected. For example:
83
+ #
84
+ # (a = 1 or b = 1) and (a = 2 or b = 2)
85
+ #
86
+ # Keep: a = 1 and b = 2, a = 2 and b = 1
87
+ # Discard: a = 1 and a = 2, b = 1 and b = 2
88
+ if merged_attrs.include?(name) && merged_attrs[name] != val
89
+ merged_attrs_set[k] = nil
90
+ break
91
+ end
92
+
93
+ merged_attrs[name] = val
94
+ end
95
+ end
96
+ end
97
+
98
+ merged_attrs_set.compact!
99
+ other.params[model] = merged_attrs_set
100
+ end
101
+
102
+ other
103
+ end
104
+
105
+ def uniq!
106
+ params.each do |_, attrs_set|
107
+ attrs_set.uniq!
108
+ end
109
+ end
110
+
111
+ def memoizable?
112
+ return false if params.empty?
113
+
114
+ params.each do |model, attrs_set|
115
+ return false if attrs_set.empty?
116
+
117
+ attrs_set.each do |attrs|
118
+ return false unless RedisMemo::MemoizeQuery
119
+ .memoized_columns(model)
120
+ .include?(attrs.keys.sort)
121
+ end
122
+ end
123
+
124
+ true
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisMemo::MemoizeQuery::CachedSelect
4
+ module ConnectionAdapter
5
+ def cacheable_query(*args)
6
+ query, binds = super(*args)
7
+
8
+ # Persist the arel object to StatementCache#execute
9
+ query.instance_variable_set(:@__redis_memo_memoize_query_memoize_query_arel, args.last)
10
+
11
+ [query, binds]
12
+ end
13
+
14
+ def exec_query(*args)
15
+ # An Arel AST in Thread local is set prior to supported query methods
16
+ if !RedisMemo.without_memo? &&
17
+ RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(args[0])
18
+ # [Reids $model Load] $sql $binds
19
+ RedisMemo::DefaultOptions.logger&.info(
20
+ "[Redis] \u001b[36;1m#{args[1]} \u001b[34;1m#{args[0]}\u001b[0m #{
21
+ args[2].map { |bind| [bind.name, bind.value_for_database]}
22
+ }"
23
+ )
24
+
25
+ super(*args)
26
+ else
27
+ RedisMemo.without_memo { super(*args) }
28
+ end
29
+ end
30
+
31
+ def select_all(*args)
32
+ if args[0].is_a?(Arel::SelectManager)
33
+ RedisMemo::MemoizeQuery::CachedSelect.current_query = args[0]
34
+ end
35
+
36
+ super(*args)
37
+ ensure
38
+ RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisMemo::MemoizeQuery::CachedSelect
4
+ module StatementCache
5
+ def execute(*args)
6
+ arel = query_builder.instance_variable_get(:@__redis_memo_memoize_query_memoize_query_arel)
7
+ RedisMemo::MemoizeQuery::CachedSelect.current_query = arel
8
+ RedisMemo::MemoizeQuery::CachedSelect.current_substitutes =
9
+ bind_map.map_substitutes(args[0])
10
+
11
+ super(*args)
12
+ ensure
13
+ RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
14
+ end
15
+ end
16
+ end
@@ -1,15 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class RedisMemo::MemoizeRecords::Invalidation
3
+ #
4
+ # Automatically invalidate memoizable when modifying ActiveRecords objects.
5
+ # You still need to invalidate memos when you are using SQL queries to perform
6
+ # update / delete (does not trigger record callbacks)
7
+ #
8
+ class RedisMemo::MemoizeQuery::Invalidation
4
9
  def self.install(model_class)
5
- var_name = :@@__redis_memo_memoize_records_invalidation_installed__
10
+ var_name = :@@__redis_memo_memoize_query_invalidation_installed__
6
11
  return if model_class.class_variable_defined?(var_name)
7
12
 
8
13
  model_class.class_eval do
9
14
  # A memory-persistent memoizable used for invalidating all queries of a
10
15
  # particular model
11
16
  def self.redis_memo_class_memoizable
12
- @redis_memo_class_memoizable ||= RedisMemo::MemoizeRecords.create_memo(self)
17
+ @redis_memo_class_memoizable ||= RedisMemo::MemoizeQuery.create_memo(self)
13
18
  end
14
19
 
15
20
  %i(delete decrement! increment!).each do |method_name|
@@ -18,7 +23,7 @@ class RedisMemo::MemoizeRecords::Invalidation
18
23
  define_method method_name do |*args|
19
24
  result = send(:"without_redis_memo_invalidation_#{method_name}", *args)
20
25
 
21
- RedisMemo::MemoizeRecords.invalidate(self)
26
+ RedisMemo::MemoizeQuery.invalidate(self)
22
27
 
23
28
  result
24
29
  end
@@ -77,7 +82,7 @@ class RedisMemo::MemoizeRecords::Invalidation
77
82
 
78
83
  define_method method_name do |*args|
79
84
  result = send(:"#{method_name}_without_redis_memo_invalidation", *args)
80
- RedisMemo::MemoizeRecords.invalidate_all(model_class)
85
+ RedisMemo::MemoizeQuery.invalidate_all(model_class)
81
86
  result
82
87
  end
83
88
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'memoize_method'
3
+
4
+ module RedisMemo::MemoizeQuery
5
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class RedisMemo::MemoizeRecords::ModelCallback
3
+ class RedisMemo::MemoizeQuery::ModelCallback
4
4
  def self.install(model_class)
5
5
  var_name = :@@__redis_memo_memoize_record_after_save_callback_installed__
6
6
  return if model_class.class_variable_defined?(var_name)
@@ -12,10 +12,10 @@ class RedisMemo::MemoizeRecords::ModelCallback
12
12
  end
13
13
 
14
14
  def after_save(record)
15
- RedisMemo::MemoizeRecords.invalidate(record)
15
+ RedisMemo::MemoizeQuery.invalidate(record)
16
16
  end
17
17
 
18
18
  def after_destroy(record)
19
- RedisMemo::MemoizeRecords.invalidate(record)
19
+ RedisMemo::MemoizeQuery.invalidate(record)
20
20
  end
21
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-memo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0.beta
4
+ version: 0.0.0.beta.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chan Zuckerberg Initiative
@@ -152,11 +152,14 @@ files:
152
152
  - lib/redis_memo/memoizable/dependency.rb
153
153
  - lib/redis_memo/memoizable/invalidation.rb
154
154
  - lib/redis_memo/memoize_method.rb
155
- - lib/redis_memo/memoize_method.rbi
156
- - lib/redis_memo/memoize_records.rb
157
- - lib/redis_memo/memoize_records/cached_select.rb
158
- - lib/redis_memo/memoize_records/invalidation.rb
159
- - lib/redis_memo/memoize_records/model_callback.rb
155
+ - lib/redis_memo/memoize_query.rb
156
+ - lib/redis_memo/memoize_query/cached_select.rb
157
+ - lib/redis_memo/memoize_query/cached_select/bind_params.rb
158
+ - lib/redis_memo/memoize_query/cached_select/connection_adapter.rb
159
+ - lib/redis_memo/memoize_query/cached_select/statement_cache.rb
160
+ - lib/redis_memo/memoize_query/invalidation.rb
161
+ - lib/redis_memo/memoize_query/memoize_table_column.rb
162
+ - lib/redis_memo/memoize_query/model_callback.rb
160
163
  - lib/redis_memo/middleware.rb
161
164
  - lib/redis_memo/options.rb
162
165
  - lib/redis_memo/redis.rb
@@ -1,10 +0,0 @@
1
- # typed: true
2
-
3
- module RedisMemo::MemoizeMethod
4
- include Kernel
5
-
6
- def class; end
7
- def name; end
8
- def alias_method(*); end
9
- def define_method(*); end
10
- end
@@ -1,146 +0,0 @@
1
- # frozen_string_literal: true
2
- require_relative 'memoize_method'
3
-
4
- #
5
- # Automatically invalidate memoizable when modifying ActiveRecords objects.
6
- # You still need to invalidate memos when you are using SQL queries to perform
7
- # update / delete (does not trigger record callbacks)
8
- #
9
- module RedisMemo::MemoizeRecords
10
- require_relative 'memoize_records/cached_select'
11
- require_relative 'memoize_records/invalidation'
12
- require_relative 'memoize_records/model_callback'
13
-
14
- # TODO: MemoizeRecords -> MemoizeQuery
15
- def memoize_records
16
- RedisMemo::MemoizeRecords.using_active_record!(self)
17
-
18
- memoize_table_column(primary_key.to_sym, editable: false)
19
- end
20
-
21
- # Only editable columns will be used to create memos that are invalidatable
22
- # after each record save
23
- def memoize_table_column(*raw_columns, editable: true)
24
- RedisMemo::MemoizeRecords.using_active_record!(self)
25
-
26
- columns = raw_columns.map(&:to_sym).sort
27
-
28
- RedisMemo::MemoizeRecords.memoized_columns(self, editable_only: true) << columns if editable
29
- RedisMemo::MemoizeRecords.memoized_columns(self, editable_only: false) << columns
30
-
31
- RedisMemo::MemoizeRecords::ModelCallback.install(self)
32
- RedisMemo::MemoizeRecords::Invalidation.install(self)
33
-
34
- if ENV['REDIS_MEMO_DISABLE_CACHED_SELECT'] != 'true'
35
- RedisMemo::MemoizeRecords::CachedSelect.install(ActiveRecord::Base.connection)
36
- end
37
-
38
- columns.each do |column|
39
- unless self.columns_hash.include?(column.to_s)
40
- raise(
41
- RedisMemo::ArgumentError,
42
- "'#{self.name}' does not contain column '#{column}'",
43
- )
44
- end
45
- end
46
- rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
47
- # no-opts: models with memoize_table_column decleared might be loaded in
48
- # rake tasks that are used to create databases
49
- end
50
-
51
- def self.using_active_record!(model_class)
52
- unless model_class.respond_to?(:<) && model_class < ActiveRecord::Base
53
- raise RedisMemo::ArgumentError, "'#{model_class.name}' does not use ActiveRecord"
54
- end
55
- end
56
-
57
- @@memoized_columns = Hash.new { |h, k| h[k] = [Set.new, Set.new] }
58
-
59
- def self.memoized_columns(model_or_table, editable_only: false)
60
- table = model_or_table.is_a?(Class) ? model_or_table.table_name : model_or_table
61
- @@memoized_columns[table.to_sym][editable_only ? 1 : 0]
62
- end
63
-
64
- # extra_props are considered as AND conditions on the model class
65
- def self.create_memo(model_class, **extra_props)
66
- RedisMemo::MemoizeRecords.using_active_record!(model_class)
67
-
68
- keys = extra_props.keys.sort
69
- if !keys.empty? && !RedisMemo::MemoizeRecords.memoized_columns(model_class).include?(keys)
70
- raise(
71
- RedisMemo::ArgumentError,
72
- "'#{model_class.name}' has not memoized columns: #{keys}",
73
- )
74
- end
75
-
76
- extra_props.each do |key, values|
77
- # The data type is ensured by the database, thus we don't need to cast
78
- # types here for better performance
79
- column_name = key.to_s
80
- values = [values] unless values.is_a?(Enumerable)
81
- extra_props[key] =
82
- if model_class.defined_enums.include?(column_name)
83
- enum_mapping = model_class.defined_enums[column_name]
84
- values.map do |value|
85
- # Assume a value is a converted enum if it does not exist in the
86
- # enum mapping
87
- (enum_mapping[value.to_s] || value).to_s
88
- end
89
- else
90
- values.map(&:to_s)
91
- end
92
- end
93
-
94
- RedisMemo::Memoizable.new(
95
- __redis_memo_memoize_records_model_class_name__: model_class.name,
96
- **extra_props,
97
- )
98
- end
99
-
100
- def self.invalidate_all(model_class)
101
- RedisMemo::Tracer.trace(
102
- 'redis_memo.memoizable.invalidate_all',
103
- model_class.name,
104
- ) do
105
- RedisMemo::Memoizable.invalidate([model_class.redis_memo_class_memoizable])
106
- end
107
- end
108
-
109
- def self.invalidate(record)
110
- # Invalidate memos with current values
111
- memos_to_invalidate = memoized_columns(record.class).map do |columns|
112
- props = {}
113
- columns.each do |column|
114
- props[column] = record.send(column)
115
- end
116
-
117
- RedisMemo::MemoizeRecords.create_memo(record.class, **props)
118
- end
119
-
120
- # Create memos with previous values if
121
- # - there are saved changes
122
- # - this is not creating a new record
123
- if !record.saved_changes.empty? && !record.saved_changes.include?(record.class.primary_key)
124
- previous_values = {}
125
- record.saved_changes.each do |column, (previous_value, _)|
126
- previous_values[column.to_sym] = previous_value
127
- end
128
-
129
- memoized_columns(record.class, editable_only: true).each do |columns|
130
- props = previous_values.slice(*columns)
131
- next if props.empty?
132
-
133
- # Fill the column values that have not changed
134
- columns.each do |column|
135
- next if props.include?(column)
136
-
137
- props[column] = record.send(column)
138
- end
139
-
140
- memos_to_invalidate << RedisMemo::MemoizeRecords.create_memo(record.class, **props)
141
- end
142
- end
143
-
144
- RedisMemo::Memoizable.invalidate(memos_to_invalidate)
145
- end
146
- end