redis-memo 0.0.0.alpha → 0.0.0.beta.4
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 +5 -0
- data/lib/redis_memo.rb +96 -1
- data/lib/redis_memo/after_commit.rb +119 -0
- data/lib/redis_memo/batch.rb +54 -0
- data/lib/redis_memo/cache.rb +108 -0
- data/lib/redis_memo/connection_pool.rb +27 -0
- data/lib/redis_memo/future.rb +125 -0
- data/lib/redis_memo/memoizable.rb +119 -0
- data/lib/redis_memo/memoizable/dependency.rb +66 -0
- data/lib/redis_memo/memoizable/invalidation.rb +142 -0
- data/lib/redis_memo/memoize_method.rb +133 -0
- data/lib/redis_memo/memoize_query.rb +151 -0
- data/lib/redis_memo/memoize_query/cached_select.rb +380 -0
- 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_query/model_callback.rb +21 -0
- data/lib/redis_memo/middleware.rb +18 -0
- data/lib/redis_memo/options.rb +87 -0
- data/lib/redis_memo/redis.rb +67 -0
- data/lib/redis_memo/tracer.rb +25 -0
- metadata +106 -14
@@ -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
|
@@ -0,0 +1,380 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Inspect a SQL's AST to memoize SELECT statements
|
5
|
+
#
|
6
|
+
# As Rails applies additional logic on top of the rows returned from the
|
7
|
+
# database:
|
8
|
+
#
|
9
|
+
# - `belongs_to ..., inverse_of: ...`: By using `inverse_of`, Rails could
|
10
|
+
# prevent instantiating the different objects from the DB when the objects are
|
11
|
+
# really the same.
|
12
|
+
#
|
13
|
+
# - Associations may have scopes that add more filtering to the existing query
|
14
|
+
#
|
15
|
+
# - +ActiveRecord::Relation+ defers the data fetching until the end
|
16
|
+
#
|
17
|
+
# - +ActiveRecord::Relation+ could preload associations to avoid N+1 queries
|
18
|
+
#
|
19
|
+
# Memoizing each SQL query by inspecting its AST is the best approach we have
|
20
|
+
# to reliably perform query caching with ActiveRecord.
|
21
|
+
#
|
22
|
+
# Here's how this works at a high level:
|
23
|
+
#
|
24
|
+
# First, we extract dependencies from SQL queries. Consider the following query
|
25
|
+
#
|
26
|
+
# SELECT * FROM my_records WHERE value = 'a'
|
27
|
+
#
|
28
|
+
# The rows returned from the database would not change unless records with the
|
29
|
+
# value 'a' have been updated. Therefore, if we are to cache this query, we
|
30
|
+
# need to set dependencies on this query and discard the cache if the
|
31
|
+
# dependencies have changed.
|
32
|
+
#
|
33
|
+
# Here's the dependency (aka a +Memoizable+) for the above query:
|
34
|
+
#
|
35
|
+
# Memoizable.new(model: MyRecord, value: 'a')
|
36
|
+
#
|
37
|
+
# We bump the column dependencies automatically when updating a record that has
|
38
|
+
# the `memoize_table_column` declaration on the model class.
|
39
|
+
#
|
40
|
+
# class MyRecord < ApplicationRecord
|
41
|
+
# extend RedisMemo::MemoizeQuery
|
42
|
+
# memoize_table_column :value
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# After saving any MyRecord, we will bump the dependencies versions filled with
|
46
|
+
# the record's current and past values:
|
47
|
+
#
|
48
|
+
# my_record.update(value: 'new_value') # from 'old_value'
|
49
|
+
#
|
50
|
+
# Then we will bump the versions for at least two memoizables:
|
51
|
+
#
|
52
|
+
# Memoizable.new(model: MyRecord, value: 'new_value')
|
53
|
+
# Memoizable.new(model: MyRecord, value: 'old_value')
|
54
|
+
#
|
55
|
+
# When the another_value column is also memoized, we have another
|
56
|
+
# memoizable to bump version for, regardless whether the another_value
|
57
|
+
# filed of my_record has been changed:
|
58
|
+
#
|
59
|
+
# Memoizable.new(model: MyRecord, another_value: 'current_value')
|
60
|
+
#
|
61
|
+
# We need to do this because other columns could be cached in
|
62
|
+
#
|
63
|
+
# SELECT * FROM ... WHERE another_value = ?
|
64
|
+
#
|
65
|
+
# queries. Those query result sets become stale after the update.
|
66
|
+
#
|
67
|
+
# By setting dependencies on the query, we will use the dependencies versions
|
68
|
+
# as a part of the query cache key. After we bump the dependencies versions,
|
69
|
+
# the following request will produce a different new query cache key, so the
|
70
|
+
# request will end up with a cache_miss:
|
71
|
+
# - Compute the fresh query result and it will actually send the query to the database
|
72
|
+
# - Fill the new query cache key with the fresh query result
|
73
|
+
#
|
74
|
+
# After saving my_record and bumping the dependencies versions, all currently
|
75
|
+
# cached SQL queries that have `value = 'new_value'` or `value = 'old_value'`
|
76
|
+
# in their WHERE clause (or any WHERE conditions that's using the current
|
77
|
+
# memoized column values of my_record) can no longer be accessed by any new
|
78
|
+
# requests; Those entries will be automatically deleted through cache expiry or
|
79
|
+
# cache eviction.
|
80
|
+
#
|
81
|
+
# We can only memoize SQL queries that can be automatically invalidated through
|
82
|
+
# this mechanism:
|
83
|
+
#
|
84
|
+
# - The query contains only =, IN conditions
|
85
|
+
# - And those conditions are on table columns that have been memoized via
|
86
|
+
# +memoized_table_column+
|
87
|
+
#
|
88
|
+
# See +extract_bind_params+ for the precise detection logic.
|
89
|
+
#
|
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
|
+
|
101
|
+
def self.install(connection)
|
102
|
+
klass = connection.class
|
103
|
+
return if klass.singleton_class < RedisMemo::MemoizeMethod
|
104
|
+
|
105
|
+
klass.class_eval do
|
106
|
+
extend RedisMemo::MemoizeMethod
|
107
|
+
|
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
|
116
|
+
.current_query_bind_params
|
117
|
+
.params
|
118
|
+
.each do |model, attrs_set|
|
119
|
+
attrs_set.each do |attrs|
|
120
|
+
depends_on model, **attrs
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
depends_on RedisMemo::Memoizable.new(
|
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
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
klass.prepend(ConnectionAdapter)
|
139
|
+
ActiveRecord::StatementCache.prepend(StatementCache)
|
140
|
+
|
141
|
+
# Cached result objects could be sampled to compare against fresh result
|
142
|
+
# objects. Overwrite the == operator to make the comparison meaningful.
|
143
|
+
ActiveRecord::Result.class_eval do
|
144
|
+
def ==(other)
|
145
|
+
columns == other.columns && rows == other.rows
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
ActiveRecord::StatementCache::BindMap.class_eval do
|
150
|
+
def map_substitutes(values)
|
151
|
+
ret = {}
|
152
|
+
@indexes.each_with_index do |offset, i|
|
153
|
+
bound_attr = @bound_attributes[offset]
|
154
|
+
substitute = bound_attr.value
|
155
|
+
ret[substitute] = values[i]
|
156
|
+
end
|
157
|
+
ret
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.extract_bind_params(sql)
|
163
|
+
ast = Thread.current[THREAD_KEY_AREL]&.ast
|
164
|
+
return false unless ast.is_a?(Arel::Nodes::SelectStatement)
|
165
|
+
return false unless ast.to_sql == sql
|
166
|
+
|
167
|
+
Thread.current[THREAD_KEY_SUBSTITUTES] ||= {}
|
168
|
+
# Iterate through the Arel AST in a Depth First Search
|
169
|
+
bind_params = extract_bind_params_recurse(ast)
|
170
|
+
return false unless bind_params
|
171
|
+
|
172
|
+
bind_params.uniq!
|
173
|
+
return false unless bind_params.memoizable?
|
174
|
+
|
175
|
+
Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = bind_params
|
176
|
+
true
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.current_query_bind_params
|
180
|
+
Thread.current[THREAD_KEY_AREL_BIND_PARAMS]
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.current_query=(arel)
|
184
|
+
Thread.current[THREAD_KEY_AREL] = arel
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.current_substitutes=(substitutes)
|
188
|
+
Thread.current[THREAD_KEY_SUBSTITUTES] = substitutes
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.reset_current_query
|
192
|
+
Thread.current[THREAD_KEY_AREL] = nil
|
193
|
+
Thread.current[THREAD_KEY_SUBSTITUTES] = nil
|
194
|
+
Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = nil
|
195
|
+
end
|
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
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
# A pre-order Depth First Search
|
213
|
+
#
|
214
|
+
# Note: Arel::Nodes#each returns a list in post-order, and it does not step
|
215
|
+
# into Union nodes. So we're implementing our own DFS
|
216
|
+
def self.extract_bind_params_recurse(node)
|
217
|
+
bind_params = BindParams.new
|
218
|
+
|
219
|
+
case node
|
220
|
+
when NodeHasFilterCondition
|
221
|
+
attr_node = node.left
|
222
|
+
return unless attr_node.is_a?(Arel::Attributes::Attribute)
|
223
|
+
|
224
|
+
table_node =
|
225
|
+
case attr_node.relation
|
226
|
+
when Arel::Table
|
227
|
+
attr_node.relation
|
228
|
+
when Arel::Nodes::TableAlias
|
229
|
+
attr_node.relation.left
|
230
|
+
else
|
231
|
+
# Not yet supported
|
232
|
+
return
|
233
|
+
end
|
234
|
+
|
235
|
+
binding_relation = extract_binding_relation(table_node)
|
236
|
+
return unless binding_relation
|
237
|
+
|
238
|
+
rights = node.right.is_a?(Array) ? node.right : [node.right]
|
239
|
+
substitutes = Thread.current[THREAD_KEY_SUBSTITUTES]
|
240
|
+
|
241
|
+
rights.each do |right|
|
242
|
+
case right
|
243
|
+
when Arel::Nodes::BindParam
|
244
|
+
# No need to type cast as they're only used to create +memoizables+
|
245
|
+
# (used as strings)
|
246
|
+
value = right.value.value_before_type_cast
|
247
|
+
|
248
|
+
if value.is_a?(ActiveRecord::StatementCache::Substitute)
|
249
|
+
value = substitutes[value]
|
250
|
+
end
|
251
|
+
|
252
|
+
bind_params.params[binding_relation] << {
|
253
|
+
right.value.name.to_sym => value,
|
254
|
+
}
|
255
|
+
when Arel::Nodes::Casted
|
256
|
+
bind_params.params[binding_relation] << {
|
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,
|
264
|
+
}
|
265
|
+
else
|
266
|
+
bind_params = bind_params.union(extract_bind_params_recurse(right))
|
267
|
+
if bind_params
|
268
|
+
next
|
269
|
+
else
|
270
|
+
return
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
bind_params
|
276
|
+
when Arel::Nodes::SelectStatement
|
277
|
+
# No OREDER BY
|
278
|
+
return unless node.orders.empty?
|
279
|
+
|
280
|
+
node.cores.each do |core|
|
281
|
+
# We don't support JOINs
|
282
|
+
return unless core.source.right.empty?
|
283
|
+
|
284
|
+
# Should have a WHERE if directly selecting from a table
|
285
|
+
source_node = core.source.left
|
286
|
+
binding_relation = nil
|
287
|
+
case source_node
|
288
|
+
when Arel::Table
|
289
|
+
binding_relation = extract_binding_relation(source_node)
|
290
|
+
|
291
|
+
return if core.wheres.empty? || binding_relation.nil?
|
292
|
+
when Arel::Nodes::TableAlias
|
293
|
+
bind_params = bind_params.union(
|
294
|
+
extract_bind_params_recurse(source_node.left)
|
295
|
+
)
|
296
|
+
|
297
|
+
return unless bind_params
|
298
|
+
else
|
299
|
+
return
|
300
|
+
end
|
301
|
+
|
302
|
+
# Binds wheres before havings
|
303
|
+
core.wheres.each do |where|
|
304
|
+
bind_params = bind_params.union(
|
305
|
+
extract_bind_params_recurse(where)
|
306
|
+
)
|
307
|
+
|
308
|
+
return unless bind_params
|
309
|
+
end
|
310
|
+
|
311
|
+
core.havings.each do |having|
|
312
|
+
bind_params = bind_params.union(
|
313
|
+
extract_bind_params_recurse(having)
|
314
|
+
)
|
315
|
+
|
316
|
+
return unless bind_params
|
317
|
+
end
|
318
|
+
|
319
|
+
# Reject any unbound select queries
|
320
|
+
return if binding_relation && bind_params.params[binding_relation].empty?
|
321
|
+
end
|
322
|
+
|
323
|
+
bind_params
|
324
|
+
when Arel::Nodes::Grouping
|
325
|
+
# Inline SQL
|
326
|
+
return if node.expr.is_a?(Arel::Nodes::SqlLiteral)
|
327
|
+
|
328
|
+
extract_bind_params_recurse(node.expr)
|
329
|
+
when Arel::Nodes::And
|
330
|
+
node.children.each do |child|
|
331
|
+
bind_params = bind_params.product(
|
332
|
+
extract_bind_params_recurse(child)
|
333
|
+
)
|
334
|
+
|
335
|
+
return unless bind_params
|
336
|
+
end
|
337
|
+
|
338
|
+
bind_params
|
339
|
+
when Arel::Nodes::Union, Arel::Nodes::Or
|
340
|
+
[node.left, node.right].each do |child|
|
341
|
+
bind_params = bind_params.union(
|
342
|
+
extract_bind_params_recurse(child)
|
343
|
+
)
|
344
|
+
|
345
|
+
return unless bind_params
|
346
|
+
end
|
347
|
+
|
348
|
+
bind_params
|
349
|
+
else
|
350
|
+
# Not yet supported
|
351
|
+
return
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def self.extract_binding_relation(table_node)
|
356
|
+
enabled_models[table_node.try(:name)]
|
357
|
+
end
|
358
|
+
|
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
|
369
|
+
else
|
370
|
+
false
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# Thread locals to exchange information between RedisMemo and ActiveRecord
|
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__
|
380
|
+
end
|