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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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