redis-memo 0.0.0.beta → 0.0.0.beta.5
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 +4 -0
- data/lib/redis_memo.rb +52 -3
- data/lib/redis_memo/after_commit.rb +6 -1
- data/lib/redis_memo/cache.rb +18 -5
- data/lib/redis_memo/connection_pool.rb +27 -0
- data/lib/redis_memo/future.rb +3 -3
- data/lib/redis_memo/memoizable.rb +7 -4
- data/lib/redis_memo/memoizable/dependency.rb +42 -12
- data/lib/redis_memo/memoizable/invalidation.rb +39 -20
- data/lib/redis_memo/memoize_method.rb +53 -13
- data/lib/redis_memo/memoize_query.rb +151 -0
- data/lib/redis_memo/{memoize_records → memoize_query}/cached_select.rb +80 -199
- 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_query/invalidation.rb +211 -0
- 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
- data/lib/redis_memo/options.rb +17 -18
- data/lib/redis_memo/redis.rb +2 -1
- data/lib/redis_memo/tracer.rb +4 -2
- metadata +46 -14
- data/lib/redis_memo/memoize_method.rbi +0 -10
- data/lib/redis_memo/memoize_records.rb +0 -146
- data/lib/redis_memo/memoize_records/invalidation.rb +0 -85
@@ -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
|
|
@@ -15,11 +15,24 @@ module RedisMemo::MemoizeMethod
|
|
15
15
|
define_method method_name_with_memo do |*args|
|
16
16
|
return send(method_name_without_memo, *args) if RedisMemo.without_memo?
|
17
17
|
|
18
|
+
dependent_memos = nil
|
19
|
+
if depends_on
|
20
|
+
dependency = RedisMemo::MemoizeMethod.get_or_extract_dependencies(self, *args, &depends_on)
|
21
|
+
dependent_memos = dependency.memos
|
22
|
+
end
|
23
|
+
|
18
24
|
future = RedisMemo::Future.new(
|
19
25
|
self,
|
20
|
-
|
26
|
+
case method_id
|
27
|
+
when NilClass
|
28
|
+
RedisMemo::MemoizeMethod.method_id(self, method_name)
|
29
|
+
when String, Symbol
|
30
|
+
method_id
|
31
|
+
else
|
32
|
+
method_id.call(self, *args)
|
33
|
+
end,
|
21
34
|
args,
|
22
|
-
|
35
|
+
dependent_memos,
|
23
36
|
options,
|
24
37
|
method_name_without_memo,
|
25
38
|
)
|
@@ -30,9 +43,25 @@ module RedisMemo::MemoizeMethod
|
|
30
43
|
end
|
31
44
|
|
32
45
|
future.execute
|
46
|
+
rescue RedisMemo::WithoutMemoization
|
47
|
+
send(method_name_without_memo, *args)
|
33
48
|
end
|
34
49
|
|
35
50
|
alias_method method_name, method_name_with_memo
|
51
|
+
|
52
|
+
@__redis_memo_method_dependencies ||= Hash.new
|
53
|
+
@__redis_memo_method_dependencies[method_name] = depends_on
|
54
|
+
|
55
|
+
define_method :dependency_of do |method_name, *method_args|
|
56
|
+
method_depends_on = self.class.instance_variable_get(:@__redis_memo_method_dependencies)[method_name]
|
57
|
+
unless method_depends_on
|
58
|
+
raise(
|
59
|
+
RedisMemo::ArgumentError,
|
60
|
+
"#{method_name} is not a memoized method"
|
61
|
+
)
|
62
|
+
end
|
63
|
+
RedisMemo::MemoizeMethod.get_or_extract_dependencies(self, *method_args, &method_depends_on)
|
64
|
+
end
|
36
65
|
end
|
37
66
|
|
38
67
|
def self.method_id(ref, method_name)
|
@@ -42,23 +71,34 @@ module RedisMemo::MemoizeMethod
|
|
42
71
|
"#{class_name}#{is_class_method ? '::' : '#'}#{method_name}"
|
43
72
|
end
|
44
73
|
|
45
|
-
def self.
|
46
|
-
|
47
|
-
future_contexts.each_with_index do |(ref, _, method_args, depends_on), i|
|
48
|
-
if depends_on
|
49
|
-
dependency = RedisMemo::Memoizable::Dependency.new
|
74
|
+
def self.extract_dependencies(ref, *method_args, &depends_on)
|
75
|
+
dependency = RedisMemo::Memoizable::Dependency.new
|
50
76
|
|
51
|
-
|
52
|
-
|
77
|
+
# Resolve the dependency recursively
|
78
|
+
dependency.instance_exec(ref, *method_args, &depends_on)
|
79
|
+
dependency
|
80
|
+
end
|
53
81
|
|
54
|
-
|
55
|
-
|
82
|
+
def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
|
83
|
+
if RedisMemo::Cache.local_dependency_cache
|
84
|
+
RedisMemo::Cache.local_dependency_cache[ref] ||= {}
|
85
|
+
RedisMemo::Cache.local_dependency_cache[ref][depends_on] ||= {}
|
86
|
+
RedisMemo::Cache.local_dependency_cache[ref][depends_on][method_args] ||= extract_dependencies(ref, *method_args, &depends_on)
|
87
|
+
else
|
88
|
+
extract_dependencies(ref, *method_args, &depends_on)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.method_cache_keys(future_contexts)
|
93
|
+
memos = Array.new(future_contexts.size)
|
94
|
+
future_contexts.each_with_index do |(_, _, dependent_memos), i|
|
95
|
+
memos[i] = dependent_memos
|
56
96
|
end
|
57
97
|
|
58
98
|
j = 0
|
59
99
|
memo_checksums = RedisMemo::Memoizable.checksums(memos.compact)
|
60
100
|
method_cache_key_versions = Array.new(future_contexts.size)
|
61
|
-
future_contexts.each_with_index do |(
|
101
|
+
future_contexts.each_with_index do |(method_id, method_args, _), i|
|
62
102
|
if memos[i]
|
63
103
|
method_cache_key_versions[i] = [method_id, memo_checksums[j]]
|
64
104
|
j += 1
|
@@ -0,0 +1,151 @@
|
|
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
|
+
return if ENV["REDIS_MEMO_DISABLE_#{self.table_name.upcase}"] == 'true'
|
17
|
+
|
18
|
+
columns = raw_columns.map(&:to_sym).sort
|
19
|
+
|
20
|
+
RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: true) << columns if editable
|
21
|
+
RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: false) << columns
|
22
|
+
|
23
|
+
RedisMemo::MemoizeQuery::ModelCallback.install(self)
|
24
|
+
RedisMemo::MemoizeQuery::Invalidation.install(self)
|
25
|
+
|
26
|
+
if ENV['REDIS_MEMO_DISABLE_CACHED_SELECT'] != 'true'
|
27
|
+
RedisMemo::MemoizeQuery::CachedSelect.install(ActiveRecord::Base.connection)
|
28
|
+
end
|
29
|
+
|
30
|
+
# The code below might fail due to missing DB/table errors
|
31
|
+
columns.each do |column|
|
32
|
+
unless self.columns_hash.include?(column.to_s)
|
33
|
+
raise(
|
34
|
+
RedisMemo::ArgumentError,
|
35
|
+
"'#{self.name}' does not contain column '#{column}'",
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
unless ENV["REDIS_MEMO_DISABLE_QUERY_#{self.table_name.upcase}"] == 'true'
|
41
|
+
RedisMemo::MemoizeQuery::CachedSelect.enabled_models[self.table_name] = self
|
42
|
+
end
|
43
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
|
44
|
+
# no-opts: models with memoize_table_column decleared might be loaded in
|
45
|
+
# rake tasks that are used to create databases
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.using_active_record!(model_class)
|
49
|
+
unless using_active_record?(model_class)
|
50
|
+
raise RedisMemo::ArgumentError, "'#{model_class.name}' does not use ActiveRecord"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.using_active_record?(model_class)
|
55
|
+
model_class.respond_to?(:<) && model_class < ActiveRecord::Base
|
56
|
+
end
|
57
|
+
|
58
|
+
@@memoized_columns = Hash.new { |h, k| h[k] = [Set.new, Set.new] }
|
59
|
+
|
60
|
+
def self.memoized_columns(model_or_table, editable_only: false)
|
61
|
+
table = model_or_table.is_a?(Class) ? model_or_table.table_name : model_or_table
|
62
|
+
@@memoized_columns[table.to_sym][editable_only ? 1 : 0]
|
63
|
+
end
|
64
|
+
|
65
|
+
# extra_props are considered as AND conditions on the model class
|
66
|
+
def self.create_memo(model_class, **extra_props)
|
67
|
+
using_active_record!(model_class)
|
68
|
+
|
69
|
+
keys = extra_props.keys.sort
|
70
|
+
if !keys.empty? && !memoized_columns(model_class).include?(keys)
|
71
|
+
raise(
|
72
|
+
RedisMemo::ArgumentError,
|
73
|
+
"'#{model_class.name}' has not memoized columns: #{keys}",
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
extra_props.each do |key, value|
|
78
|
+
# The data type is ensured by the database, thus we don't need to cast
|
79
|
+
# types here for better performance
|
80
|
+
column_name = key.to_s
|
81
|
+
extra_props[key] =
|
82
|
+
if model_class.defined_enums.include?(column_name)
|
83
|
+
enum_mapping = model_class.defined_enums[column_name]
|
84
|
+
# Assume a value is a converted enum if it does not exist in the
|
85
|
+
# enum mapping
|
86
|
+
(enum_mapping[value.to_s] || value).to_s
|
87
|
+
else
|
88
|
+
value.to_s
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
RedisMemo::Memoizable.new(
|
93
|
+
__redis_memo_memoize_query_table_name__: model_class.table_name,
|
94
|
+
**extra_props,
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.invalidate_all(model_class)
|
99
|
+
RedisMemo::Tracer.trace(
|
100
|
+
'redis_memo.memoizable.invalidate_all',
|
101
|
+
model_class.name,
|
102
|
+
) do
|
103
|
+
RedisMemo::Memoizable.invalidate([model_class.redis_memo_class_memoizable])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.invalidate(*records)
|
108
|
+
RedisMemo::Memoizable.invalidate(
|
109
|
+
records.map { |record| to_memos(record) }.flatten,
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.to_memos(record)
|
114
|
+
# Invalidate memos with current values
|
115
|
+
memos_to_invalidate = memoized_columns(record.class).map do |columns|
|
116
|
+
props = {}
|
117
|
+
columns.each do |column|
|
118
|
+
props[column] = record.send(column)
|
119
|
+
end
|
120
|
+
|
121
|
+
create_memo(record.class, **props)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Create memos with previous values if
|
125
|
+
# - there are saved changes
|
126
|
+
# - this is not creating a new record
|
127
|
+
if !record.saved_changes.empty? && !record.saved_changes.include?(record.class.primary_key)
|
128
|
+
previous_values = {}
|
129
|
+
record.saved_changes.each do |column, (previous_value, _)|
|
130
|
+
previous_values[column.to_sym] = previous_value
|
131
|
+
end
|
132
|
+
|
133
|
+
memoized_columns(record.class, editable_only: true).each do |columns|
|
134
|
+
props = previous_values.slice(*columns)
|
135
|
+
next if props.empty?
|
136
|
+
|
137
|
+
# Fill the column values that have not changed
|
138
|
+
columns.each do |column|
|
139
|
+
next if props.include?(column)
|
140
|
+
|
141
|
+
props[column] = record.send(column)
|
142
|
+
end
|
143
|
+
|
144
|
+
memos_to_invalidate << create_memo(record.class, **props)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
memos_to_invalidate
|
149
|
+
end
|
150
|
+
end
|
151
|
+
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,15 @@ 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 do |bind|
|
127
|
+
if bind.respond_to?(:value_for_database)
|
128
|
+
bind.value_for_database
|
129
|
+
else
|
130
|
+
# In activerecord >= 6, a bind could be an actual database value
|
131
|
+
bind
|
132
|
+
end
|
133
|
+
end
|
112
134
|
)
|
113
135
|
end
|
114
136
|
end
|
@@ -137,64 +159,6 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
137
159
|
end
|
138
160
|
end
|
139
161
|
|
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
162
|
def self.extract_bind_params(sql)
|
199
163
|
ast = Thread.current[THREAD_KEY_AREL]&.ast
|
200
164
|
return false unless ast.is_a?(Arel::Nodes::SelectStatement)
|
@@ -230,6 +194,19 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
230
194
|
Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = nil
|
231
195
|
end
|
232
196
|
|
197
|
+
def self.with_new_query_context
|
198
|
+
prev_arel = Thread.current[THREAD_KEY_AREL]
|
199
|
+
prev_substitutes = Thread.current[THREAD_KEY_SUBSTITUTES]
|
200
|
+
prev_bind_params = Thread.current[THREAD_KEY_AREL_BIND_PARAMS]
|
201
|
+
RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
|
202
|
+
|
203
|
+
yield
|
204
|
+
ensure
|
205
|
+
Thread.current[THREAD_KEY_AREL] = prev_arel
|
206
|
+
Thread.current[THREAD_KEY_SUBSTITUTES] = prev_substitutes
|
207
|
+
Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = prev_bind_params
|
208
|
+
end
|
209
|
+
|
233
210
|
private
|
234
211
|
|
235
212
|
# A pre-order Depth First Search
|
@@ -240,7 +217,7 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
240
217
|
bind_params = BindParams.new
|
241
218
|
|
242
219
|
case node
|
243
|
-
when
|
220
|
+
when NodeHasFilterCondition
|
244
221
|
attr_node = node.left
|
245
222
|
return unless attr_node.is_a?(Arel::Attributes::Attribute)
|
246
223
|
|
@@ -255,16 +232,8 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
255
232
|
return
|
256
233
|
end
|
257
234
|
|
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
|
235
|
+
binding_relation = extract_binding_relation(table_node)
|
236
|
+
return unless binding_relation
|
268
237
|
|
269
238
|
rights = node.right.is_a?(Array) ? node.right : [node.right]
|
270
239
|
substitutes = Thread.current[THREAD_KEY_SUBSTITUTES]
|
@@ -285,7 +254,13 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
285
254
|
}
|
286
255
|
when Arel::Nodes::Casted
|
287
256
|
bind_params.params[binding_relation] << {
|
288
|
-
right.attribute.name.to_sym =>
|
257
|
+
right.attribute.name.to_sym =>
|
258
|
+
if right.respond_to?(:val)
|
259
|
+
right.val
|
260
|
+
else
|
261
|
+
# activerecord >= 6
|
262
|
+
right.value
|
263
|
+
end,
|
289
264
|
}
|
290
265
|
else
|
291
266
|
bind_params = bind_params.union(extract_bind_params_recurse(right))
|
@@ -303,11 +278,17 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
303
278
|
return unless node.orders.empty?
|
304
279
|
|
305
280
|
node.cores.each do |core|
|
281
|
+
# We don't support JOINs
|
282
|
+
return unless core.source.right.empty?
|
283
|
+
|
306
284
|
# Should have a WHERE if directly selecting from a table
|
307
285
|
source_node = core.source.left
|
286
|
+
binding_relation = nil
|
308
287
|
case source_node
|
309
288
|
when Arel::Table
|
310
|
-
|
289
|
+
binding_relation = extract_binding_relation(source_node)
|
290
|
+
|
291
|
+
return if core.wheres.empty? || binding_relation.nil?
|
311
292
|
when Arel::Nodes::TableAlias
|
312
293
|
bind_params = bind_params.union(
|
313
294
|
extract_bind_params_recurse(source_node.left)
|
@@ -334,6 +315,9 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
334
315
|
|
335
316
|
return unless bind_params
|
336
317
|
end
|
318
|
+
|
319
|
+
# Reject any unbound select queries
|
320
|
+
return if binding_relation && bind_params.params[binding_relation].empty?
|
337
321
|
end
|
338
322
|
|
339
323
|
bind_params
|
@@ -352,7 +336,7 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
352
336
|
end
|
353
337
|
|
354
338
|
bind_params
|
355
|
-
when Arel::Nodes::
|
339
|
+
when Arel::Nodes::Union, Arel::Nodes::Or
|
356
340
|
[node.left, node.right].each do |child|
|
357
341
|
bind_params = bind_params.union(
|
358
342
|
extract_bind_params_recurse(child)
|
@@ -368,132 +352,29 @@ class RedisMemo::MemoizeRecords::CachedSelect
|
|
368
352
|
end
|
369
353
|
end
|
370
354
|
|
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
|
355
|
+
def self.extract_binding_relation(table_node)
|
356
|
+
enabled_models[table_node.try(:name)]
|
357
|
+
end
|
393
358
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
359
|
+
class NodeHasFilterCondition
|
360
|
+
def self.===(node)
|
361
|
+
case node
|
362
|
+
when Arel::Nodes::Equality, Arel::Nodes::In
|
363
|
+
true
|
364
|
+
else
|
365
|
+
# In activerecord >= 6, a new arel node HomogeneousIn is introduced
|
366
|
+
if defined?(Arel::Nodes::HomogeneousIn) &&
|
367
|
+
node.is_a?(Arel::Nodes::HomogeneousIn)
|
368
|
+
true
|
401
369
|
else
|
402
|
-
|
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
|
370
|
+
false
|
463
371
|
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
372
|
end
|
476
373
|
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
|
493
374
|
end
|
494
375
|
|
495
376
|
# Thread locals to exchange information between RedisMemo and ActiveRecord
|
496
|
-
THREAD_KEY_AREL = :
|
497
|
-
THREAD_KEY_SUBSTITUTES = :
|
498
|
-
THREAD_KEY_AREL_BIND_PARAMS = :
|
377
|
+
THREAD_KEY_AREL = :__redis_memo_memoize_query_cached_select_arel__
|
378
|
+
THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_query_cached_select_substitues__
|
379
|
+
THREAD_KEY_AREL_BIND_PARAMS = :__redis_memo_memoize_query_cached_select_arel_bind_params__
|
499
380
|
end
|