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 +4 -4
- data/lib/redis_memo.rb +1 -1
- data/lib/redis_memo/memoizable/dependency.rb +1 -1
- data/lib/redis_memo/memoize_method.rb +9 -2
- data/lib/redis_memo/memoize_query.rb +142 -0
- data/lib/redis_memo/{memoize_records → memoize_query}/cached_select.rb +40 -202
- data/lib/redis_memo/memoize_query/cached_select/bind_params.rb +127 -0
- data/lib/redis_memo/memoize_query/cached_select/connection_adapter.rb +41 -0
- data/lib/redis_memo/memoize_query/cached_select/statement_cache.rb +16 -0
- data/lib/redis_memo/{memoize_records → memoize_query}/invalidation.rb +10 -5
- data/lib/redis_memo/memoize_query/memoize_table_column.rb +5 -0
- data/lib/redis_memo/{memoize_records → memoize_query}/model_callback.rb +3 -3
- metadata +9 -6
- data/lib/redis_memo/memoize_method.rbi +0 -10
- data/lib/redis_memo/memoize_records.rb +0 -146
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e64eaaecf192519453c1accb0d10ca25ed2cf8274c10b6cb128a14e9a2e616e
|
4
|
+
data.tar.gz: fd98fce04f77660b8fc5960e491695504ef438394a882fb80d36b706038d10e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20ca8f3efa4ca8591d073960c5fd5b9c8fbcd920b58ae6fe378ec3511af082c2cf964c72cda93739b6bef7294d5be9ceb77eb6000cb3df567f18b2bd581cf4c8
|
7
|
+
data.tar.gz: 1c485024e2eb3f541139bbd8f136279f5b1010b7b81ab30929d2e87a8917c4f0d8fb35d2ae39d94fd14d2f918b6476f53d589026f4412a8b118d6b2b6d732a93
|
data/lib/redis_memo.rb
CHANGED
@@ -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::
|
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
|
-
|
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::
|
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::
|
91
|
-
|
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
|
100
|
-
|
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
|
-
|
111
|
-
|
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
|
-
|
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
|
-
|
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::
|
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
|
-
|
372
|
-
|
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 = :
|
497
|
-
THREAD_KEY_SUBSTITUTES = :
|
498
|
-
THREAD_KEY_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
|
-
|
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 = :@@
|
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::
|
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::
|
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::
|
85
|
+
RedisMemo::MemoizeQuery.invalidate_all(model_class)
|
81
86
|
result
|
82
87
|
end
|
83
88
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class RedisMemo::
|
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::
|
15
|
+
RedisMemo::MemoizeQuery.invalidate(record)
|
16
16
|
end
|
17
17
|
|
18
18
|
def after_destroy(record)
|
19
|
-
RedisMemo::
|
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/
|
156
|
-
- lib/redis_memo/
|
157
|
-
- lib/redis_memo/
|
158
|
-
- lib/redis_memo/
|
159
|
-
- lib/redis_memo/
|
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,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
|