redis-memo 0.1.4 → 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,11 +274,7 @@ 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
 
@@ -281,7 +294,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
281
294
  return if core.wheres.empty? || binding_relation.nil?
282
295
  when Arel::Nodes::TableAlias
283
296
  bind_params = bind_params.union(
284
- extract_bind_params_recurse(source_node.left)
297
+ extract_bind_params_recurse(source_node.left),
285
298
  )
286
299
 
287
300
  return unless bind_params
@@ -292,7 +305,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
292
305
  # Binds wheres before havings
293
306
  core.wheres.each do |where|
294
307
  bind_params = bind_params.union(
295
- extract_bind_params_recurse(where)
308
+ extract_bind_params_recurse(where),
296
309
  )
297
310
 
298
311
  return unless bind_params
@@ -300,7 +313,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
300
313
 
301
314
  core.havings.each do |having|
302
315
  bind_params = bind_params.union(
303
- extract_bind_params_recurse(having)
316
+ extract_bind_params_recurse(having),
304
317
  )
305
318
 
306
319
  return unless bind_params
@@ -313,13 +326,13 @@ class RedisMemo::MemoizeQuery::CachedSelect
313
326
  bind_params
314
327
  when Arel::Nodes::Grouping
315
328
  # Inline SQL
316
- return if node.expr.is_a?(Arel::Nodes::SqlLiteral)
317
-
318
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
319
332
  when Arel::Nodes::And
320
333
  node.children.each do |child|
321
334
  bind_params = bind_params.product(
322
- extract_bind_params_recurse(child)
335
+ extract_bind_params_recurse(child),
323
336
  )
324
337
 
325
338
  return unless bind_params
@@ -329,28 +342,33 @@ class RedisMemo::MemoizeQuery::CachedSelect
329
342
  when Arel::Nodes::Union, Arel::Nodes::Or
330
343
  [node.left, node.right].each do |child|
331
344
  bind_params = bind_params.union(
332
- extract_bind_params_recurse(child)
345
+ extract_bind_params_recurse(child),
333
346
  )
334
347
 
335
348
  return unless bind_params
336
349
  end
337
350
 
338
351
  bind_params
339
-
340
- when Arel::Nodes::NotEqual
341
- # We don't cache based on NOT queries (where.not) because it is unbound
342
- # but we memoize queries with NOT and other bound queries, so we return the original bind_params
343
- return bind_params
344
352
  else
345
353
  # Not yet supported
346
- return
354
+ nil
347
355
  end
356
+ # rubocop: enable Lint/NonLocalExitFromIterator
348
357
  end
349
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]
350
365
  def self.extract_binding_relation(table_node)
351
366
  enabled_models[table_node.try(:name)]
352
367
  end
353
368
 
369
+ #
370
+ # Identify whether the node has filter condition
371
+ #
354
372
  class NodeHasFilterCondition
355
373
  def self.===(node)
356
374
  case node
@@ -367,9 +385,4 @@ class RedisMemo::MemoizeQuery::CachedSelect
367
385
  end
368
386
  end
369
387
  end
370
-
371
- # Thread locals to exchange information between RedisMemo and ActiveRecord
372
- THREAD_KEY_AREL = :__redis_memo_memoize_query_cached_select_arel__
373
- THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_query_cached_select_substitues__
374
- THREAD_KEY_AREL_BIND_PARAMS = :__redis_memo_memoize_query_cached_select_arel_bind_params__
375
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
@@ -260,7 +262,7 @@ class RedisMemo::MemoizeQuery::Invalidation
260
262
  'redis_memo.memoize_query.invalidation',
261
263
  "#{__method__}##{model_class.name}",
262
264
  ) do
263
- RedisMemo.without_memo { relation.reload }
265
+ RedisMemo.without_memoization { relation.reload }
264
266
  end
265
267
  end
266
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
@@ -9,7 +9,7 @@ class RedisMemo::Middleware
9
9
  result = nil
10
10
 
11
11
  RedisMemo::Cache.with_local_cache do
12
- RedisMemo.with_max_connection_attempts(ENV['REDIS_MEMO_MAX_ATTEMPTS_PER_REQUEST']&.to_i) do
12
+ RedisMemo.with_max_connection_attempts(RedisMemo::DefaultOptions.max_connection_attempts) do
13
13
  result = @app.call(env)
14
14
  end
15
15
  end