redis-memo 0.1.0 → 1.0.0

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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
3
+ ##
4
4
  # Inspect a SQL's AST to memoize SELECT statements
5
5
  #
6
6
  # As Rails applies additional logic on top of the rows returned from the
@@ -94,6 +94,12 @@ class RedisMemo::MemoizeQuery::CachedSelect
94
94
 
95
95
  @@enabled_models = {}
96
96
 
97
+ # Thread locals to exchange information between RedisMemo and ActiveRecord
98
+ RedisMemo::ThreadLocalVar.define :arel
99
+ RedisMemo::ThreadLocalVar.define :substitues
100
+ RedisMemo::ThreadLocalVar.define :arel_bind_params
101
+
102
+ # @return [Hash] models enabled for caching
97
103
  def self.enabled_models
98
104
  @@enabled_models
99
105
  end
@@ -107,9 +113,11 @@ class RedisMemo::MemoizeQuery::CachedSelect
107
113
 
108
114
  memoize_method(
109
115
  :exec_query,
110
- method_id: proc do |_, sql, *args|
111
- sql.gsub(/(\$\d+)/, '?') # $1 -> ?
112
- .gsub(/((, *)*\?)+/, '?') # (?, ?, ? ...) -> (?)
116
+ method_id: proc do |_, sql, *_args|
117
+ # replace $1 with ?,
118
+ # and (?, ?, ? ...) with (?)
119
+ sql.gsub(/(\$\d+)/, '?')
120
+ .gsub(/((, *)*\?)+/, '?')
113
121
  end,
114
122
  ) do |_, sql, _, binds, **|
115
123
  depends_on RedisMemo::MemoizeQuery::CachedSelect.current_query_bind_params
@@ -123,7 +131,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
123
131
  # In activerecord >= 6, a bind could be an actual database value
124
132
  bind
125
133
  end
126
- end
134
+ end,
127
135
  )
128
136
  end
129
137
  end
@@ -152,12 +160,18 @@ class RedisMemo::MemoizeQuery::CachedSelect
152
160
  end
153
161
  end
154
162
 
163
+ # Extract bind params from the query by inspecting the SQL's AST recursively
164
+ # The bind params will be passed into the local thread variables
165
+ # See +extract_bind_params_recurse+ for how to extract binding params recursively
166
+ #
167
+ # @param sql [String] SQL query
168
+ # @return [Boolean] indicating whether a query should be cached
155
169
  def self.extract_bind_params(sql)
156
- ast = Thread.current[THREAD_KEY_AREL]&.ast
170
+ ast = RedisMemo::ThreadLocalVar.arel&.ast
157
171
  return false unless ast.is_a?(Arel::Nodes::SelectStatement)
158
172
  return false unless ast.to_sql == sql
159
173
 
160
- Thread.current[THREAD_KEY_SUBSTITUTES] ||= {}
174
+ RedisMemo::ThreadLocalVar.substitues ||= {}
161
175
  # Iterate through the Arel AST in a Depth First Search
162
176
  bind_params = extract_bind_params_recurse(ast)
163
177
  return false unless bind_params
@@ -165,48 +179,51 @@ class RedisMemo::MemoizeQuery::CachedSelect
165
179
  bind_params.uniq!
166
180
  return false unless bind_params.memoizable?
167
181
 
168
- Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = bind_params
182
+ RedisMemo::ThreadLocalVar.arel_bind_params = bind_params
169
183
  true
170
184
  end
171
185
 
172
186
  def self.current_query_bind_params
173
- Thread.current[THREAD_KEY_AREL_BIND_PARAMS]
187
+ RedisMemo::ThreadLocalVar.arel_bind_params
174
188
  end
175
189
 
176
190
  def self.current_query=(arel)
177
- Thread.current[THREAD_KEY_AREL] = arel
191
+ RedisMemo::ThreadLocalVar.arel = arel
178
192
  end
179
193
 
180
194
  def self.current_substitutes=(substitutes)
181
- Thread.current[THREAD_KEY_SUBSTITUTES] = substitutes
195
+ RedisMemo::ThreadLocalVar.substitues = substitutes
182
196
  end
183
197
 
184
198
  def self.reset_current_query
185
- Thread.current[THREAD_KEY_AREL] = nil
186
- Thread.current[THREAD_KEY_SUBSTITUTES] = nil
187
- Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = nil
199
+ RedisMemo::ThreadLocalVar.arel = nil
200
+ RedisMemo::ThreadLocalVar.substitues = nil
201
+ RedisMemo::ThreadLocalVar.arel_bind_params = nil
188
202
  end
189
203
 
190
204
  def self.with_new_query_context
191
- prev_arel = Thread.current[THREAD_KEY_AREL]
192
- prev_substitutes = Thread.current[THREAD_KEY_SUBSTITUTES]
193
- prev_bind_params = Thread.current[THREAD_KEY_AREL_BIND_PARAMS]
205
+ prev_arel = RedisMemo::ThreadLocalVar.arel
206
+ prev_substitutes = RedisMemo::ThreadLocalVar.substitues
207
+ prev_bind_params = RedisMemo::ThreadLocalVar.arel_bind_params
194
208
  RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
195
209
 
196
210
  yield
197
211
  ensure
198
- Thread.current[THREAD_KEY_AREL] = prev_arel
199
- Thread.current[THREAD_KEY_SUBSTITUTES] = prev_substitutes
200
- Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = prev_bind_params
212
+ RedisMemo::ThreadLocalVar.arel = prev_arel
213
+ RedisMemo::ThreadLocalVar.substitues = prev_substitutes
214
+ RedisMemo::ThreadLocalVar.arel_bind_params = prev_bind_params
201
215
  end
202
216
 
203
- private
204
-
205
217
  # A pre-order Depth First Search
206
218
  #
207
219
  # Note: Arel::Nodes#each returns a list in post-order, and it does not step
208
220
  # into Union nodes. So we're implementing our own DFS
221
+ #
222
+ # @param node [Arel::Nodes::Node]
223
+ #
224
+ # @return [RedisMemo::MemoizeQuery::CachedSelect::BindParams]
209
225
  def self.extract_bind_params_recurse(node)
226
+ # rubocop: disable Lint/NonLocalExitFromIterator
210
227
  bind_params = BindParams.new
211
228
 
212
229
  case node
@@ -229,7 +246,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
229
246
  return unless binding_relation
230
247
 
231
248
  rights = node.right.is_a?(Array) ? node.right : [node.right]
232
- substitutes = Thread.current[THREAD_KEY_SUBSTITUTES]
249
+ substitutes = RedisMemo::ThreadLocalVar.substitues
233
250
 
234
251
  rights.each do |right|
235
252
  case right
@@ -257,19 +274,12 @@ class RedisMemo::MemoizeQuery::CachedSelect
257
274
  }
258
275
  else
259
276
  bind_params = bind_params.union(extract_bind_params_recurse(right))
260
- if bind_params
261
- next
262
- else
263
- return
264
- end
277
+ return if !bind_params
265
278
  end
266
279
  end
267
280
 
268
281
  bind_params
269
282
  when Arel::Nodes::SelectStatement
270
- # No OREDER BY
271
- return unless node.orders.empty?
272
-
273
283
  node.cores.each do |core|
274
284
  # We don't support JOINs
275
285
  return unless core.source.right.empty?
@@ -284,7 +294,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
284
294
  return if core.wheres.empty? || binding_relation.nil?
285
295
  when Arel::Nodes::TableAlias
286
296
  bind_params = bind_params.union(
287
- extract_bind_params_recurse(source_node.left)
297
+ extract_bind_params_recurse(source_node.left),
288
298
  )
289
299
 
290
300
  return unless bind_params
@@ -295,7 +305,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
295
305
  # Binds wheres before havings
296
306
  core.wheres.each do |where|
297
307
  bind_params = bind_params.union(
298
- extract_bind_params_recurse(where)
308
+ extract_bind_params_recurse(where),
299
309
  )
300
310
 
301
311
  return unless bind_params
@@ -303,7 +313,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
303
313
 
304
314
  core.havings.each do |having|
305
315
  bind_params = bind_params.union(
306
- extract_bind_params_recurse(having)
316
+ extract_bind_params_recurse(having),
307
317
  )
308
318
 
309
319
  return unless bind_params
@@ -316,13 +326,13 @@ class RedisMemo::MemoizeQuery::CachedSelect
316
326
  bind_params
317
327
  when Arel::Nodes::Grouping
318
328
  # Inline SQL
319
- return if node.expr.is_a?(Arel::Nodes::SqlLiteral)
320
-
321
329
  extract_bind_params_recurse(node.expr)
330
+ when Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual, Arel::Nodes::NotEqual
331
+ bind_params
322
332
  when Arel::Nodes::And
323
333
  node.children.each do |child|
324
334
  bind_params = bind_params.product(
325
- extract_bind_params_recurse(child)
335
+ extract_bind_params_recurse(child),
326
336
  )
327
337
 
328
338
  return unless bind_params
@@ -332,7 +342,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
332
342
  when Arel::Nodes::Union, Arel::Nodes::Or
333
343
  [node.left, node.right].each do |child|
334
344
  bind_params = bind_params.union(
335
- extract_bind_params_recurse(child)
345
+ extract_bind_params_recurse(child),
336
346
  )
337
347
 
338
348
  return unless bind_params
@@ -341,14 +351,24 @@ class RedisMemo::MemoizeQuery::CachedSelect
341
351
  bind_params
342
352
  else
343
353
  # Not yet supported
344
- return
354
+ nil
345
355
  end
356
+ # rubocop: enable Lint/NonLocalExitFromIterator
346
357
  end
347
358
 
359
+ # Retrieve the model info from the table node
360
+ # table node is an Arel::Table object, e.g. <Arel::Table @name="sites" ...>
361
+ # and we can retrieve the model info by inspecting thhe table name
362
+ # See +RedisMemo::MemoizeQuery::memoize_table_column+ for how to construct enabled_models
363
+ #
364
+ # @params table_node [Arel::Table]
348
365
  def self.extract_binding_relation(table_node)
349
366
  enabled_models[table_node.try(:name)]
350
367
  end
351
368
 
369
+ #
370
+ # Identify whether the node has filter condition
371
+ #
352
372
  class NodeHasFilterCondition
353
373
  def self.===(node)
354
374
  case node
@@ -365,9 +385,4 @@ class RedisMemo::MemoizeQuery::CachedSelect
365
385
  end
366
386
  end
367
387
  end
368
-
369
- # Thread locals to exchange information between RedisMemo and ActiveRecord
370
- THREAD_KEY_AREL = :__redis_memo_memoize_query_cached_select_arel__
371
- THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_query_cached_select_substitues__
372
- THREAD_KEY_AREL_BIND_PARAMS = :__redis_memo_memoize_query_cached_select_arel_bind_params__
373
388
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  class RedisMemo::MemoizeQuery::CachedSelect
4
4
  module ConnectionAdapter
5
- def cacheable_query(*args)
5
+ ruby2_keywords def cacheable_query(*args)
6
6
  query, binds = super(*args)
7
7
 
8
8
  # Persist the arel object to StatementCache#execute
@@ -11,24 +11,24 @@ class RedisMemo::MemoizeQuery::CachedSelect
11
11
  [query, binds]
12
12
  end
13
13
 
14
- def exec_query(*args)
14
+ ruby2_keywords def exec_query(*args)
15
15
  # An Arel AST in Thread local is set prior to supported query methods
16
- if !RedisMemo.without_memo? &&
16
+ if !RedisMemo.without_memoization? &&
17
17
  RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(args[0])
18
18
  # [Reids $model Load] $sql $binds
19
19
  RedisMemo::DefaultOptions.logger&.info(
20
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
- }"
21
+ args[2].map { |bind| [bind.name, bind.value_for_database] }
22
+ }",
23
23
  )
24
24
 
25
25
  super(*args)
26
26
  else
27
- RedisMemo.without_memo { super(*args) }
27
+ RedisMemo.without_memoization { super(*args) }
28
28
  end
29
29
  end
30
30
 
31
- def select_all(*args)
31
+ ruby2_keywords def select_all(*args)
32
32
  if args[0].is_a?(Arel::SelectManager)
33
33
  RedisMemo::MemoizeQuery::CachedSelect.current_query = args[0]
34
34
  end
@@ -17,28 +17,29 @@ class RedisMemo::MemoizeQuery::Invalidation
17
17
  @redis_memo_class_memoizable ||= RedisMemo::MemoizeQuery.create_memo(self)
18
18
  end
19
19
 
20
- %i(delete decrement! increment!).each do |method_name|
20
+ %i[delete decrement! increment!].each do |method_name|
21
21
  alias_method :"without_redis_memo_invalidation_#{method_name}", method_name
22
22
 
23
23
  define_method method_name do |*args|
24
- result = send(:"without_redis_memo_invalidation_#{method_name}", *args)
24
+ result = __send__(:"without_redis_memo_invalidation_#{method_name}", *args)
25
25
 
26
26
  RedisMemo::MemoizeQuery.invalidate(self)
27
27
 
28
28
  result
29
29
  end
30
+ ruby2_keywords method_name
30
31
  end
31
32
  end
32
33
 
33
34
  # Methods that won't trigger model callbacks
34
35
  # https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks
35
- %i(
36
+ %i[
36
37
  decrement_counter
37
38
  delete_all delete_by
38
39
  increment_counter
39
40
  touch_all
40
41
  update_column update_columns update_all update_counters
41
- ).each do |method_name|
42
+ ].each do |method_name|
42
43
  # Example: Model.update_all
43
44
  rewrite_default_method(
44
45
  model_class,
@@ -56,27 +57,27 @@ class RedisMemo::MemoizeQuery::Invalidation
56
57
  )
57
58
  end
58
59
 
59
- %i(
60
+ %i[
60
61
  insert insert! insert_all insert_all!
61
- ).each do |method_name|
62
+ ].each do |method_name|
62
63
  rewrite_insert_method(
63
64
  model_class,
64
65
  method_name,
65
66
  )
66
67
  end
67
68
 
68
- %i(
69
+ %i[
69
70
  upsert upsert_all
70
- ).each do |method_name|
71
+ ].each do |method_name|
71
72
  rewrite_upsert_method(
72
73
  model_class,
73
74
  method_name,
74
75
  )
75
76
  end
76
77
 
77
- %i(
78
+ %i[
78
79
  import import!
79
- ).each do |method_name|
80
+ ].each do |method_name|
80
81
  rewrite_import_method(
81
82
  model_class,
82
83
  method_name,
@@ -116,8 +117,6 @@ class RedisMemo::MemoizeQuery::Invalidation
116
117
  result
117
118
  end
118
119
 
119
- private
120
-
121
120
  #
122
121
  # There’s no good way to perform fine-grind cache invalidation when
123
122
  # operations are bulk update operations such as update_all, and delete_all
@@ -127,17 +126,18 @@ class RedisMemo::MemoizeQuery::Invalidation
127
126
  #
128
127
  def self.rewrite_default_method(model_class, klass, method_name, class_method:)
129
128
  methods = class_method ? :methods : :instance_methods
130
- return unless klass.send(methods).include?(method_name)
129
+ return unless klass.__send__(methods).include?(method_name)
131
130
 
132
131
  klass = klass.singleton_class if class_method
133
132
  klass.class_eval do
134
133
  alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
135
134
 
136
135
  define_method method_name do |*args|
137
- result = send(:"#{method_name}_without_redis_memo_invalidation", *args)
136
+ result = __send__(:"#{method_name}_without_redis_memo_invalidation", *args)
138
137
  RedisMemo::MemoizeQuery.invalidate_all(model_class)
139
138
  result
140
139
  end
140
+ ruby2_keywords method_name
141
141
  end
142
142
  end
143
143
 
@@ -149,9 +149,10 @@ class RedisMemo::MemoizeQuery::Invalidation
149
149
 
150
150
  define_method method_name do |*args, &blk|
151
151
  RedisMemo::MemoizeQuery::Invalidation.invalidate_new_records(model_class) do
152
- send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
152
+ __send__(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
153
153
  end
154
154
  end
155
+ ruby2_keywords method_name
155
156
  end
156
157
  end
157
158
 
@@ -169,7 +170,7 @@ class RedisMemo::MemoizeQuery::Invalidation
169
170
  # HEAD (6.1.3)
170
171
  conflict_target: nil,
171
172
  ) do
172
- send(
173
+ __send__(
173
174
  :"#{method_name}_without_redis_memo_invalidation",
174
175
  attributes,
175
176
  unique_by: unique_by,
@@ -216,9 +217,10 @@ class RedisMemo::MemoizeQuery::Invalidation
216
217
  records: records,
217
218
  conflict_target: conflict_target,
218
219
  ) do
219
- send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
220
+ __send__(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
220
221
  end
221
222
  end
223
+ ruby2_keywords method_name
222
224
  end
223
225
  end
224
226
 
@@ -228,7 +230,7 @@ class RedisMemo::MemoizeQuery::Invalidation
228
230
  records.each do |record|
229
231
  conditions = {}
230
232
  conflict_target.each do |column|
231
- conditions[column] = record.send(column)
233
+ conditions[column] = record.__send__(column)
232
234
  end
233
235
  if or_chain
234
236
  or_chain = or_chain.or(model_class.where(conditions))
@@ -245,7 +247,7 @@ class RedisMemo::MemoizeQuery::Invalidation
245
247
  'redis_memo.memoize_query.invalidation',
246
248
  "#{__method__}##{model_class.name}",
247
249
  ) do
248
- RedisMemo.without_memo do
250
+ RedisMemo.without_memoization do
249
251
  model_class.where(
250
252
  model_class.arel_table[model_class.primary_key].gt(target_id),
251
253
  ).to_a
@@ -254,11 +256,13 @@ class RedisMemo::MemoizeQuery::Invalidation
254
256
  end
255
257
 
256
258
  def self.select_by_conflict_target_relation(model_class, relation)
259
+ return [] unless relation
260
+
257
261
  RedisMemo::Tracer.trace(
258
262
  'redis_memo.memoize_query.invalidation',
259
263
  "#{__method__}##{model_class.name}",
260
264
  ) do
261
- RedisMemo.without_memo { relation.reload }
265
+ RedisMemo.without_memoization { relation.reload }
262
266
  end
263
267
  end
264
268
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'memoize_method'
3
4
 
4
5
  module RedisMemo::MemoizeQuery