activerecord-refined 0.11.0 → 0.12.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.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/README.md +19 -22
- data/activerecord-refined.gemspec +4 -3
- data/docs/conditions.md +3 -1
- data/docs/expressions.md +10 -7
- data/docs/index.md +1 -1
- data/examples/expressions.rb +8 -8
- data/lib/active_record/refined/ast/arithmetics.rb +137 -0
- data/lib/active_record/refined/ast/conditions.rb +451 -0
- data/lib/active_record/refined/ast/expressions.rb +304 -0
- data/lib/active_record/refined/ast/functions.rb +254 -0
- data/lib/active_record/refined/ast/grouping.rb +63 -0
- data/lib/active_record/refined/ast/json.rb +530 -0
- data/lib/active_record/refined/ast/node.rb +298 -0
- data/lib/active_record/refined/ast/ordering.rb +104 -0
- data/lib/active_record/refined/ast/predications.rb +384 -0
- data/lib/active_record/refined/ast/windows.rb +126 -0
- data/lib/active_record/refined/ast.rb +10 -2610
- data/lib/active_record/refined/block_context.rb +671 -0
- data/lib/active_record/refined/block_syntax.rb +128 -0
- data/lib/active_record/refined/query_methods.rb +444 -0
- data/lib/active_record/refined/version.rb +8 -0
- data/lib/active_record/refined/writes.rb +72 -0
- data/lib/active_record/refined.rb +7 -1307
- data/lib/activerecord-refined.rb +0 -4
- metadata +35 -5
- data/lib/activerecord-refined/version.rb +0 -12
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/refined/ast/node"
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module Refined
|
|
7
|
+
module AST
|
|
8
|
+
# A plain SQL comparison. The value is passed through as it is, so a Range
|
|
9
|
+
# or an Array compares against a PostgreSQL range or array column, the way
|
|
10
|
+
# Active Record's own force_equality? types do.
|
|
11
|
+
class Comparison < Predicate
|
|
12
|
+
# @private
|
|
13
|
+
OPERATOR_MAP = {
|
|
14
|
+
:== => :eq, :!= => :not_eq,
|
|
15
|
+
:> => :gt, :>= => :gteq, :< => :lt, :<= => :lteq
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# @private
|
|
19
|
+
attr_reader :column, :operator, :value
|
|
20
|
+
|
|
21
|
+
def initialize(column, operator, value)
|
|
22
|
+
@column = column
|
|
23
|
+
@operator = operator
|
|
24
|
+
@value = value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @private
|
|
28
|
+
def to_arel(table, model)
|
|
29
|
+
arel_column = to_arel_operand(column, table, model)
|
|
30
|
+
arel_value =
|
|
31
|
+
case value
|
|
32
|
+
when Node then value.to_arel(table, model)
|
|
33
|
+
when ::Symbol then column_operand(value, table, model)
|
|
34
|
+
when ActiveRecord::Relation then scalar_subquery(value)
|
|
35
|
+
else quote_number(value)
|
|
36
|
+
end
|
|
37
|
+
arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
# A relation compared against a column has to yield a single value, so
|
|
42
|
+
# unlike In there is no sensible default select list to fall back on.
|
|
43
|
+
def scalar_subquery(relation)
|
|
44
|
+
if relation.select_values.empty?
|
|
45
|
+
raise ArgumentError,
|
|
46
|
+
"#{operator} needs a subquery selecting one value; add a select"
|
|
47
|
+
end
|
|
48
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
49
|
+
relation.arel
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# IS TRUE, IS FALSE and their negations, which every adapter spells the
|
|
54
|
+
# same way and answers alike, NULL included.
|
|
55
|
+
class TruthValue < Predicate
|
|
56
|
+
# @private
|
|
57
|
+
attr_reader :operand, :value, :negated
|
|
58
|
+
|
|
59
|
+
def initialize(operand, value, negated: false)
|
|
60
|
+
@operand = operand
|
|
61
|
+
@value = value
|
|
62
|
+
@negated = negated
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @private
|
|
66
|
+
def to_arel(table, model)
|
|
67
|
+
Dialect.for(model).truth_value(
|
|
68
|
+
to_arel_operand(operand, table, model), value, negated, model)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A relation standing for a set of values, which is what IN and the
|
|
73
|
+
# quantifiers each take. The treatment is Active Record's own
|
|
74
|
+
# RelationHandler's: without an explicit select list the subquery
|
|
75
|
+
# selects the model's primary key.
|
|
76
|
+
# @private
|
|
77
|
+
module SetSubquery
|
|
78
|
+
private
|
|
79
|
+
def set_subquery(relation, spelling)
|
|
80
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
81
|
+
if relation.select_values.empty?
|
|
82
|
+
model = relation.model
|
|
83
|
+
if model.composite_primary_key?
|
|
84
|
+
raise ArgumentError,
|
|
85
|
+
"Cannot map composite primary key #{model.primary_key} to #{spelling}"
|
|
86
|
+
end
|
|
87
|
+
relation = relation.select(relation.table[model.primary_key])
|
|
88
|
+
end
|
|
89
|
+
relation.arel
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
94
|
+
# relation.
|
|
95
|
+
class In < Predicate
|
|
96
|
+
include SetSubquery
|
|
97
|
+
|
|
98
|
+
# Range holds its endpoints to Comparable, which a quoted node is
|
|
99
|
+
# not, so this quacks the three methods Arel's between reads.
|
|
100
|
+
# @private
|
|
101
|
+
QuotedRange = Struct.new(:begin, :end, :exclude_end) do
|
|
102
|
+
def exclude_end? = exclude_end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @private
|
|
106
|
+
attr_reader :operand, :values, :negated
|
|
107
|
+
|
|
108
|
+
def initialize(operand, values, negated: false)
|
|
109
|
+
@operand = operand
|
|
110
|
+
@values = values
|
|
111
|
+
@negated = negated
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @private
|
|
115
|
+
def to_arel(table, model)
|
|
116
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
117
|
+
case values
|
|
118
|
+
when Range, QuotedRange
|
|
119
|
+
lower = quote_value(values.begin, table, model)
|
|
120
|
+
upper = quote_value(values.end, table, model)
|
|
121
|
+
if json_between?(model) && lower && upper && !negated
|
|
122
|
+
return arel_operand.gteq(lower).and(
|
|
123
|
+
values.exclude_end? ? arel_operand.lt(upper) : arel_operand.lteq(upper))
|
|
124
|
+
end
|
|
125
|
+
range = QuotedRange.new(lower, upper, values.exclude_end?)
|
|
126
|
+
arel_operand.public_send(negated ? :not_between : :between, range)
|
|
127
|
+
when ActiveRecord::Relation
|
|
128
|
+
arel_operand.public_send(negated ? :not_in : :in, set_subquery(values, "IN"))
|
|
129
|
+
else
|
|
130
|
+
arg = values
|
|
131
|
+
if arg.is_a?(::Array)
|
|
132
|
+
arg = arg.map { |value| quote_value(value, table, model) }
|
|
133
|
+
return json_list(arel_operand, arg) if json_list?(model)
|
|
134
|
+
end
|
|
135
|
+
arel_operand.public_send(negated ? :not_in : :in, arg)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
# An element that is already an expression resolves, a symbol is a
|
|
141
|
+
# column here as everywhere, a number is quoted as itself, and the
|
|
142
|
+
# rest ride for Arel to cast by the column.
|
|
143
|
+
def quote_value(value, table, model)
|
|
144
|
+
case value
|
|
145
|
+
when Node then value.to_arel(table, model)
|
|
146
|
+
when ::Symbol then column_operand(value, table, model)
|
|
147
|
+
else quote_number(value)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# MySQL leaves IN and BETWEEN out of its JSON comparisons -- they
|
|
152
|
+
# fall back to another comparison entirely -- so on it a JSON set is
|
|
153
|
+
# spelled as the comparisons it means: the closed range as its two
|
|
154
|
+
# bounds, the list as one equality per element. That names the dug
|
|
155
|
+
# value once per element, the price SQLite's XOR pays per operand;
|
|
156
|
+
# a negated range needs nothing, Arel writing it as two comparisons
|
|
157
|
+
# everywhere. MariaDB never gets this far: the endpoints refuse as
|
|
158
|
+
# they resolve.
|
|
159
|
+
def json_between?(model)
|
|
160
|
+
(values.begin.is_a?(JsonLiteral) || values.end.is_a?(JsonLiteral)) &&
|
|
161
|
+
Dialect.for(model).json_list_by_element?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def json_list?(model)
|
|
165
|
+
values.any? { |value| value.is_a?(JsonLiteral) } &&
|
|
166
|
+
Dialect.for(model).json_list_by_element?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def json_list(arel_operand, elements)
|
|
170
|
+
comparisons = elements.map do |element|
|
|
171
|
+
negated ? arel_operand.not_eq(element) : arel_operand.eq(element)
|
|
172
|
+
end
|
|
173
|
+
joined = comparisons.inject do |so_far, piece|
|
|
174
|
+
negated ? so_far.and(piece) : so_far.or(piece)
|
|
175
|
+
end
|
|
176
|
+
negated ? Arel::Nodes::Grouping.new(joined) : joined
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# ANY and ALL, which stand on the right of a comparison and say how many
|
|
181
|
+
# of the subquery's rows have to satisfy it. Where a scalar subquery
|
|
182
|
+
# has to return one row, these take as many as come.
|
|
183
|
+
class Quantified < Node
|
|
184
|
+
include SetSubquery
|
|
185
|
+
|
|
186
|
+
# @private
|
|
187
|
+
attr_reader :kind, :relation
|
|
188
|
+
|
|
189
|
+
def initialize(kind, relation)
|
|
190
|
+
unless relation.is_a?(ActiveRecord::Relation)
|
|
191
|
+
raise ArgumentError,
|
|
192
|
+
"#{kind} takes a relation as its subquery; a list is what in? takes"
|
|
193
|
+
end
|
|
194
|
+
@kind = kind
|
|
195
|
+
@relation = relation
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The subquery goes in as its own AST rather than as the manager,
|
|
199
|
+
# which would parenthesise it a second time -- and to PostgreSQL
|
|
200
|
+
# `ANY ((SELECT ...))` is ANY of one scalar, which it refuses.
|
|
201
|
+
# @private
|
|
202
|
+
def to_arel(_table, _model)
|
|
203
|
+
Arel::Nodes::NamedFunction.new(kind, [set_subquery(relation, kind).ast])
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# EXISTS (SELECT ...) for a relation. Correlate the subquery with the
|
|
208
|
+
# outer table through qualified columns. EXISTS only asks whether a row
|
|
209
|
+
# comes back, so unlike In there is no select list to fix up.
|
|
210
|
+
class Exists < Predicate
|
|
211
|
+
# @private
|
|
212
|
+
attr_reader :relation
|
|
213
|
+
|
|
214
|
+
def initialize(relation)
|
|
215
|
+
@relation = relation
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @private
|
|
219
|
+
def to_arel(_table, _model)
|
|
220
|
+
subquery = relation
|
|
221
|
+
if subquery.eager_loading?
|
|
222
|
+
subquery = subquery.send(:apply_join_dependency)
|
|
223
|
+
end
|
|
224
|
+
subquery.arel.exists
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# `LIKE`, negated or case-insensitive as asked. The pattern of
|
|
229
|
+
# {Predications#like?} goes as written, `%` and `_` its wildcards; the
|
|
230
|
+
# shortcuts, {Predications#start_with?} and its kin, escape theirs and
|
|
231
|
+
# say so with `ESCAPE`, since SQLite reads no escape character unless
|
|
232
|
+
# told one.
|
|
233
|
+
class Like < Predicate
|
|
234
|
+
# @private
|
|
235
|
+
ESCAPE = "\\"
|
|
236
|
+
|
|
237
|
+
# Escapes % and _ so that they match literally. The pattern built from
|
|
238
|
+
# the result must be used with ESCAPE, since SQLite has no default
|
|
239
|
+
# escape character.
|
|
240
|
+
# @private
|
|
241
|
+
def self.escape(string)
|
|
242
|
+
ActiveRecord::Base.sanitize_sql_like(string, ESCAPE)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# ORs one LIKE per pattern, for the shortcuts that accept several
|
|
246
|
+
# literals the way String#start_with? does.
|
|
247
|
+
# @private
|
|
248
|
+
def self.any(operand, patterns)
|
|
249
|
+
patterns.map { |pattern| new(operand, pattern, ESCAPE) }.
|
|
250
|
+
inject { |left, right| Or.new(left, right) }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# @private
|
|
254
|
+
attr_reader :operand, :pattern, :escape, :case_sensitive, :negated
|
|
255
|
+
|
|
256
|
+
def initialize(operand, pattern, escape = nil, case_sensitive: true,
|
|
257
|
+
negated: false)
|
|
258
|
+
@operand = operand
|
|
259
|
+
@pattern = pattern
|
|
260
|
+
@escape = escape
|
|
261
|
+
@case_sensitive = case_sensitive
|
|
262
|
+
@negated = negated
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @private
|
|
266
|
+
def to_arel(table, model)
|
|
267
|
+
# Arel matches case-insensitively unless told otherwise, which is
|
|
268
|
+
# what picks ILIKE over LIKE on PostgreSQL.
|
|
269
|
+
to_arel_operand(operand, table, model).
|
|
270
|
+
public_send(negated ? :does_not_match : :matches,
|
|
271
|
+
pattern, escape, case_sensitive)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# IS [NOT] DISTINCT FROM, spelled IS / IS NOT on SQLite and <=> on
|
|
276
|
+
# MySQL. NULL compares as a value here, which is what separates these
|
|
277
|
+
# from = and <>.
|
|
278
|
+
class DistinctFrom < Predicate
|
|
279
|
+
# @private
|
|
280
|
+
attr_reader :operand, :value, :negated
|
|
281
|
+
|
|
282
|
+
def initialize(operand, value, negated: false)
|
|
283
|
+
@operand = operand
|
|
284
|
+
@value = value
|
|
285
|
+
@negated = negated
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# @private
|
|
289
|
+
def to_arel(table, model)
|
|
290
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
291
|
+
arel_value = value.is_a?(Node) ? value.to_arel(table, model) : value
|
|
292
|
+
if negated
|
|
293
|
+
arel_operand.is_distinct_from(arel_value)
|
|
294
|
+
else
|
|
295
|
+
arel_operand.is_not_distinct_from(arel_value)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Comparisons against a PostgreSQL array column, named after the Ruby
|
|
301
|
+
# methods that mean the same thing: member? is Enumerable's element
|
|
302
|
+
# test, superset? and subset? are Set's whole-array containment, and
|
|
303
|
+
# intersect? is Array's "any element in common". Each name maps to one
|
|
304
|
+
# operator; the elements are rendered as an array literal, which
|
|
305
|
+
# PostgreSQL coerces to the column's element type, so any expression
|
|
306
|
+
# works as the operand and no schema lookup is needed.
|
|
307
|
+
class ArrayPredicate < Predicate
|
|
308
|
+
# The whole-array comparisons take the collection kinds their
|
|
309
|
+
# namesakes compare against: an Array, or a Set for the Set methods.
|
|
310
|
+
# @private
|
|
311
|
+
def self.elements(arg, method_name)
|
|
312
|
+
case arg
|
|
313
|
+
when ::Array then arg
|
|
314
|
+
when ::Set then arg.to_a
|
|
315
|
+
else
|
|
316
|
+
raise ArgumentError, "#{method_name} takes an Array or Set of elements"
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# @private
|
|
321
|
+
attr_reader :operand, :operator, :elements
|
|
322
|
+
|
|
323
|
+
def initialize(operand, operator, elements)
|
|
324
|
+
@operand = operand
|
|
325
|
+
@operator = operator
|
|
326
|
+
@elements = elements
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# @private
|
|
330
|
+
#
|
|
331
|
+
# The refusal is the gem's rather than Arel's: the visitor stopped
|
|
332
|
+
# @> and && off PostgreSQL, but <@ rode an InfixOperation and
|
|
333
|
+
# rendered anywhere, so subset? alone reached the other servers.
|
|
334
|
+
def to_arel(table, model)
|
|
335
|
+
unless Dialect.for(model).array_comparisons_supported?
|
|
336
|
+
raise NotImplementedError,
|
|
337
|
+
"the array comparisons have no equivalent on " \
|
|
338
|
+
"#{model.connection_db_config.adapter}"
|
|
339
|
+
end
|
|
340
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
341
|
+
quoted = Arel::Nodes.build_quoted(array_literal)
|
|
342
|
+
case operator
|
|
343
|
+
when :"@>" then Arel::Nodes::Contains.new(arel_operand, quoted)
|
|
344
|
+
when :"&&" then Arel::Nodes::Overlaps.new(arel_operand, quoted)
|
|
345
|
+
else Arel::Nodes::InfixOperation.new(operator, arel_operand, quoted)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
private
|
|
350
|
+
# PostgreSQL array input syntax: elements joined by commas inside
|
|
351
|
+
# braces, and an element is double-quoted whenever it is empty, spells
|
|
352
|
+
# NULL, or contains a character the parser treats specially.
|
|
353
|
+
def array_literal
|
|
354
|
+
encoded = elements.map do |value|
|
|
355
|
+
s = value.to_s
|
|
356
|
+
if s.empty? || s.casecmp?("null") || s.match?(/[\s{},"\\]/)
|
|
357
|
+
"\"#{s.gsub(/["\\]/) { |c| "\\#{c}" }}\""
|
|
358
|
+
else
|
|
359
|
+
s
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
"{#{encoded.join(',')}}"
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Regular expression match: REGEXP on MySQL, ~ on PostgreSQL. SQLite has
|
|
367
|
+
# no regexp operator built in, so Arel raises NotImplementedError there.
|
|
368
|
+
class Match < Predicate
|
|
369
|
+
# @private
|
|
370
|
+
attr_reader :operand, :pattern, :negated
|
|
371
|
+
|
|
372
|
+
def initialize(operand, pattern, negated: false)
|
|
373
|
+
@operand = operand
|
|
374
|
+
@pattern = pattern.is_a?(Regexp) ? regexp_source(pattern) : pattern
|
|
375
|
+
@negated = negated
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# @private
|
|
379
|
+
def to_arel(table, model)
|
|
380
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
381
|
+
if negated
|
|
382
|
+
arel_operand.does_not_match_regexp(pattern)
|
|
383
|
+
else
|
|
384
|
+
arel_operand.matches_regexp(pattern)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private
|
|
389
|
+
# A Regexp literal reads naturally with =~, but only its source crosses
|
|
390
|
+
# over; the database has its own dialect and no notion of Ruby's flags.
|
|
391
|
+
# Dropping a flag would silently change what the query matches, so
|
|
392
|
+
# anything beyond a plain literal is refused rather than ignored.
|
|
393
|
+
def regexp_source(regexp)
|
|
394
|
+
unless regexp.options.zero?
|
|
395
|
+
raise ArgumentError,
|
|
396
|
+
"#{regexp.inspect} has options that SQL cannot express; " \
|
|
397
|
+
"pass the pattern as a string instead"
|
|
398
|
+
end
|
|
399
|
+
regexp.source
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# `AND`, which `&` between two conditions builds.
|
|
404
|
+
class And < Predicate
|
|
405
|
+
# @private
|
|
406
|
+
attr_reader :left, :right
|
|
407
|
+
|
|
408
|
+
def initialize(left, right)
|
|
409
|
+
@left = AST.check_condition(left, :&)
|
|
410
|
+
@right = AST.check_condition(right, :&)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# @private
|
|
414
|
+
def to_arel(table, model)
|
|
415
|
+
left.to_arel(table, model).and(right.to_arel(table, model))
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# `OR`, which `|` between two conditions builds.
|
|
420
|
+
class Or < Predicate
|
|
421
|
+
# @private
|
|
422
|
+
attr_reader :left, :right
|
|
423
|
+
|
|
424
|
+
def initialize(left, right)
|
|
425
|
+
@left = AST.check_condition(left, :|)
|
|
426
|
+
@right = AST.check_condition(right, :|)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# @private
|
|
430
|
+
def to_arel(table, model)
|
|
431
|
+
left.to_arel(table, model).or(right.to_arel(table, model))
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# `NOT`, which `!` on a condition builds.
|
|
436
|
+
class Not < Predicate
|
|
437
|
+
# @private
|
|
438
|
+
attr_reader :operand
|
|
439
|
+
|
|
440
|
+
def initialize(operand)
|
|
441
|
+
@operand = AST.check_condition(operand, :!)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# @private
|
|
445
|
+
def to_arel(table, model)
|
|
446
|
+
Arel::Nodes::Not.new(operand.to_arel(table, model))
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|