ransack 5.0.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +13 -27
- data/lib/ransack/active_record/base.rb +154 -0
- data/lib/ransack/active_record/context.rb +504 -0
- data/lib/ransack/active_record/dialect.rb +83 -0
- data/lib/ransack/active_record/join.rb +75 -0
- data/lib/ransack/active_record/join_association.rb +104 -0
- data/lib/ransack/active_record/join_dependency.rb +123 -0
- data/lib/ransack/active_record/reflection.rb +18 -0
- data/lib/ransack/active_record/tree_node.rb +13 -0
- data/lib/ransack/active_record.rb +44 -9
- data/lib/ransack/configuration.rb +49 -1
- data/lib/ransack/constants.rb +61 -0
- data/lib/ransack/context.rb +170 -35
- data/lib/ransack/helpers/form_builder.rb +30 -25
- data/lib/ransack/nodes/bindable.rb +0 -1
- data/lib/ransack/nodes/condition.rb +215 -73
- data/lib/ransack/nodes/grouping.rb +35 -11
- data/lib/ransack/nodes/sort.rb +1 -1
- data/lib/ransack/nodes/value.rb +8 -1
- data/lib/ransack/predicate.rb +8 -1
- data/lib/ransack/search.rb +52 -9
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack.rb +4 -1
- metadata +11 -13
- data/lib/polyamorous/activerecord/join_association.rb +0 -70
- data/lib/polyamorous/activerecord/join_association_7_2.rb +0 -55
- data/lib/polyamorous/activerecord/join_dependency.rb +0 -102
- data/lib/polyamorous/activerecord/reflection.rb +0 -11
- data/lib/polyamorous/join.rb +0 -70
- data/lib/polyamorous/polyamorous.rb +0 -27
- data/lib/polyamorous/swapping_reflection_class.rb +0 -11
- data/lib/polyamorous/tree_node.rb +0 -7
- data/lib/ransack/adapters/active_record/base.rb +0 -152
- data/lib/ransack/adapters/active_record/context.rb +0 -375
|
@@ -9,6 +9,18 @@ module Ransack
|
|
|
9
9
|
|
|
10
10
|
attr_accessor :predicate
|
|
11
11
|
|
|
12
|
+
# The effective null sentinel, captured when the condition is built. A
|
|
13
|
+
# Context can be shared by several searches, each with its own
|
|
14
|
+
# per-search override; reading the context lazily at SQL generation or
|
|
15
|
+
# form rendering time would let a later search's setting rewrite an
|
|
16
|
+
# earlier search's conditions.
|
|
17
|
+
attr_reader :null_sentinel
|
|
18
|
+
|
|
19
|
+
def initialize(context)
|
|
20
|
+
super
|
|
21
|
+
@null_sentinel = context&.null_sentinel
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
class << self
|
|
13
25
|
def extract(context, key, values)
|
|
14
26
|
attributes, predicate, combinator =
|
|
@@ -24,7 +36,7 @@ module Ransack
|
|
|
24
36
|
)
|
|
25
37
|
# TODO: Figure out what to do with multiple types of attributes,
|
|
26
38
|
# if anything. Tempted to go with "garbage in, garbage out" here.
|
|
27
|
-
if predicate.validate(condition.values, condition.default_type)
|
|
39
|
+
if predicate.validate(condition.values, condition.default_type, condition.null_sentinel)
|
|
28
40
|
condition
|
|
29
41
|
else
|
|
30
42
|
nil
|
|
@@ -44,29 +56,41 @@ module Ransack
|
|
|
44
56
|
end
|
|
45
57
|
|
|
46
58
|
if context.present?
|
|
47
|
-
str = context.
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
combinator =
|
|
51
|
-
if str.match(/_(or|and)_/)
|
|
52
|
-
$1
|
|
53
|
-
else
|
|
54
|
-
nil
|
|
59
|
+
str = context.resolve_aliases(str)
|
|
55
60
|
end
|
|
56
61
|
|
|
62
|
+
# A name that is an attribute in its own right is never split,
|
|
63
|
+
# whatever it contains.
|
|
57
64
|
if context.present? && context.attribute_method?(str)
|
|
58
65
|
attributes = [str]
|
|
66
|
+
combinator = nil
|
|
59
67
|
else
|
|
68
|
+
combinator = combinator_in(str, context)
|
|
60
69
|
attributes = str.split(/_and_|_or_/)
|
|
61
70
|
end
|
|
62
71
|
|
|
63
72
|
[attributes, predicate, combinator]
|
|
64
73
|
end
|
|
74
|
+
|
|
75
|
+
# A condition name carries one combinator: `a_or_b_or_c` or
|
|
76
|
+
# `a_and_b`. There is no grammar for precedence between the two,
|
|
77
|
+
# so a name using both is rejected under a strict search rather
|
|
78
|
+
# than silently applying whichever came first to every attribute
|
|
79
|
+
# (#1019). Mixed logic is what groupings are for.
|
|
80
|
+
def combinator_in(str, context)
|
|
81
|
+
combinators = str.scan(/_(or|and)_/).flatten.uniq
|
|
82
|
+
if combinators.size > 1 && context&.strict_conditions?
|
|
83
|
+
raise InvalidSearchError,
|
|
84
|
+
"Mixed combinators in #{str}: a condition name may use " \
|
|
85
|
+
"_and_ or _or_ but not both; use groupings for mixed logic"
|
|
86
|
+
end
|
|
87
|
+
combinators.first
|
|
88
|
+
end
|
|
65
89
|
end
|
|
66
90
|
|
|
67
91
|
def valid?
|
|
68
92
|
attributes.detect(&:valid?) && predicate && valid_arity? &&
|
|
69
|
-
predicate.validate(values, default_type) && valid_combinator?
|
|
93
|
+
predicate.validate(values, default_type, null_sentinel) && valid_combinator?
|
|
70
94
|
end
|
|
71
95
|
|
|
72
96
|
def valid_arity?
|
|
@@ -84,14 +108,14 @@ module Ransack
|
|
|
84
108
|
args.each do |attr|
|
|
85
109
|
if attr.is_a?(Hash) && (attr.key?(:name) || attr.key?("name"))
|
|
86
110
|
attr = attr.with_indifferent_access
|
|
87
|
-
|
|
111
|
+
build_attributes(attr[:name], attr[:ransacker_args])
|
|
88
112
|
else
|
|
89
|
-
|
|
113
|
+
build_attributes(attr)
|
|
90
114
|
end
|
|
91
115
|
end
|
|
92
116
|
when Hash
|
|
93
117
|
args.each do |index, attrs|
|
|
94
|
-
|
|
118
|
+
build_attributes(attrs[:name], attrs[:ransacker_args])
|
|
95
119
|
end
|
|
96
120
|
else
|
|
97
121
|
raise ArgumentError,
|
|
@@ -126,12 +150,21 @@ module Ransack
|
|
|
126
150
|
end
|
|
127
151
|
alias :v= :values=
|
|
128
152
|
|
|
153
|
+
# A supplied combinator wins; the one an expanded alias carried is the
|
|
154
|
+
# fallback, so `a: { '0' => { name: 'term' } }` with `term` aliasing
|
|
155
|
+
# `name_or_email` ORs its two attributes without an `m` in the params.
|
|
156
|
+
# The fallback covers a missing or blank `m` only: an unknown one that
|
|
157
|
+
# `Node#combinator=` normalised to nil still fails `valid_combinator?`,
|
|
158
|
+
# as it does for any other multi-attribute condition.
|
|
129
159
|
def combinator
|
|
130
|
-
@attributes.size > 1
|
|
160
|
+
return nil unless @attributes.size > 1
|
|
161
|
+
|
|
162
|
+
@combinator || (@alias_combinator unless @combinator_supplied)
|
|
131
163
|
end
|
|
132
164
|
|
|
133
165
|
def combinator=(val)
|
|
134
166
|
super
|
|
167
|
+
@combinator_supplied = !val.to_s.strip.empty?
|
|
135
168
|
end
|
|
136
169
|
|
|
137
170
|
alias :m= :combinator=
|
|
@@ -155,6 +188,25 @@ module Ransack
|
|
|
155
188
|
# TODO: Add test coverage for this behavior and ensure that `name.nil?`
|
|
156
189
|
# isn't fixing issue #701 by introducing untested regressions.
|
|
157
190
|
#
|
|
191
|
+
# An attribute named in advanced-search params (`a: { '0' => { name:
|
|
192
|
+
# 'term' } }`) arrives raw, unlike a simple key, which `Condition.extract`
|
|
193
|
+
# resolves before it gets here. So a `ransack_alias` is expanded here the
|
|
194
|
+
# same way: an alias to one attribute is built under that name, and an
|
|
195
|
+
# alias to a compound becomes one attribute per segment, joined by the
|
|
196
|
+
# compound's combinator (#1728). Left alone, the alias passed `valid?`
|
|
197
|
+
# by being allowlisted and bound a column that does not exist.
|
|
198
|
+
def build_attributes(name, ransacker_args = [])
|
|
199
|
+
return build_attribute(name, ransacker_args) if name.blank? || @context.nil?
|
|
200
|
+
|
|
201
|
+
resolved = @context.resolve_aliases(name.to_s)
|
|
202
|
+
if resolved == name.to_s || @context.attribute_method?(resolved)
|
|
203
|
+
build_attribute(resolved, ransacker_args)
|
|
204
|
+
else
|
|
205
|
+
@alias_combinator = resolved[/_(or|and)_/, 1]
|
|
206
|
+
resolved.split(/_and_|_or_/).each { |segment| build_attribute(segment, ransacker_args) }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
158
210
|
def build_attribute(name = nil, ransacker_args = [])
|
|
159
211
|
Attribute.new(@context, name, ransacker_args).tap do |attribute|
|
|
160
212
|
@context.bind(attribute, attribute.name)
|
|
@@ -173,22 +225,37 @@ module Ransack
|
|
|
173
225
|
|
|
174
226
|
def value
|
|
175
227
|
if predicate.wants_array
|
|
176
|
-
values.map { |v| v
|
|
228
|
+
values.map { |v| cast_unless_null_sentinel(v) }
|
|
177
229
|
else
|
|
178
|
-
values.first
|
|
230
|
+
cast_unless_null_sentinel(values.first)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Casting the sentinel here would turn it into a real value on a
|
|
235
|
+
# typed column (e.g. an integer column casts it to 0), so a form
|
|
236
|
+
# re-rendering this condition's current value would resubmit that
|
|
237
|
+
# literal instead of the sentinel.
|
|
238
|
+
def cast_unless_null_sentinel(v)
|
|
239
|
+
if Constants.null_sentinel_predicate?(predicate.name, null_sentinel) &&
|
|
240
|
+
Constants.null_sentinel_value?(v.value, null_sentinel)
|
|
241
|
+
v.value
|
|
242
|
+
else
|
|
243
|
+
v.cast(default_type)
|
|
179
244
|
end
|
|
180
245
|
end
|
|
181
246
|
|
|
182
247
|
# The long spellings route through the short setters: `p=` resolves a
|
|
183
248
|
# Predicate object, whereas the `predicate=` attribute writer would store
|
|
184
|
-
# the bare name.
|
|
249
|
+
# the bare name. `predicate_name` is the spelling the ransacker docs
|
|
250
|
+
# used for years, so it is accepted too (#1009).
|
|
185
251
|
LONG_KEYS = {
|
|
186
|
-
'attributes' => 'a', 'values' => 'v', 'predicate' => 'p',
|
|
252
|
+
'attributes' => 'a', 'values' => 'v', 'predicate' => 'p',
|
|
253
|
+
'predicate_name' => 'p', 'combinator' => 'm'
|
|
187
254
|
}.freeze
|
|
188
255
|
|
|
189
256
|
def build(params)
|
|
190
257
|
params.with_indifferent_access.each do |key, value|
|
|
191
|
-
if key.match(/^(a|v|p|m|attributes|values|predicate|combinator)$/)
|
|
258
|
+
if key.match(/^(a|v|p|m|attributes|values|predicate|predicate_name|combinator)$/)
|
|
192
259
|
self.send("#{LONG_KEYS.fetch(key, key)}=", value)
|
|
193
260
|
end
|
|
194
261
|
end
|
|
@@ -220,7 +287,7 @@ module Ransack
|
|
|
220
287
|
|
|
221
288
|
def predicate_name=(name)
|
|
222
289
|
self.predicate = Predicate.named(name)
|
|
223
|
-
|
|
290
|
+
if predicate && !negative?
|
|
224
291
|
attributes.each { |a| context.lock_association(a.parent) }
|
|
225
292
|
end
|
|
226
293
|
@predicate
|
|
@@ -233,7 +300,17 @@ module Ransack
|
|
|
233
300
|
alias :p :predicate_name
|
|
234
301
|
|
|
235
302
|
def validated_values
|
|
236
|
-
values.select { |v| predicate.validator.call(v.value) }
|
|
303
|
+
selected = values.select { |v| predicate.validator.call(v.value) }
|
|
304
|
+
return selected unless null_sentinel_requested?
|
|
305
|
+
|
|
306
|
+
# The sentinel is a marker to detect, never a value to search for
|
|
307
|
+
# or to cast to the column's type, so it is excluded from the
|
|
308
|
+
# values #format_predicate builds the ordinary IN/EQ node from.
|
|
309
|
+
selected.reject { |v| Constants.null_sentinel_value?(v.value, null_sentinel) }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def null_sentinel_requested?
|
|
313
|
+
Constants.null_sentinel_requested?(predicate.name, values, null_sentinel)
|
|
237
314
|
end
|
|
238
315
|
|
|
239
316
|
def casted_values_for_attribute(attr)
|
|
@@ -270,12 +347,18 @@ module Ransack
|
|
|
270
347
|
end
|
|
271
348
|
end
|
|
272
349
|
|
|
350
|
+
# The column (or ransacker expression) a predicate compares against.
|
|
351
|
+
# A case-insensitive predicate lowers it, unless the dialect has a
|
|
352
|
+
# case-insensitive LIKE of its own (ILIKE), in which case the lowering
|
|
353
|
+
# is left to the database. LOWER() is applied as a function rather than
|
|
354
|
+
# through Arel's Attribute#lower so that a ransacker returning any Arel
|
|
355
|
+
# node — a SQL literal, an infix operation — is lowered too (#1357).
|
|
273
356
|
def attr_value_for_attribute(attr)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
357
|
+
if predicate.case_insensitive && !context.dialect.case_insensitive_like?
|
|
358
|
+
Arel::Nodes::NamedFunction.new('LOWER'.freeze, [attr.attr])
|
|
359
|
+
else
|
|
360
|
+
attr.attr
|
|
361
|
+
end
|
|
279
362
|
end
|
|
280
363
|
|
|
281
364
|
def default_type
|
|
@@ -318,7 +401,7 @@ module Ransack
|
|
|
318
401
|
Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
|
|
319
402
|
end
|
|
320
403
|
when 'not_cont'
|
|
321
|
-
query.where(attribute.
|
|
404
|
+
query.where(like_predicate(attribute, 'matches'.freeze))
|
|
322
405
|
Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
|
|
323
406
|
else
|
|
324
407
|
query.where(format_predicate(attribute).not)
|
|
@@ -328,19 +411,6 @@ module Ransack
|
|
|
328
411
|
format_predicate(attribute)
|
|
329
412
|
end
|
|
330
413
|
|
|
331
|
-
# Applied per attribute rather than to the reduced node: once several
|
|
332
|
-
# attributes are combined, the result is an And/Or whose `right` is
|
|
333
|
-
# another predicate node rather than a Casted value, so
|
|
334
|
-
# replace_right_node? returns false and nothing is unwrapped at all.
|
|
335
|
-
if replace_right_node?(predicate)
|
|
336
|
-
# Replace right node object to plain integer value in order to avoid
|
|
337
|
-
# ActiveModel::RangeError from Arel::Node::Casted.
|
|
338
|
-
# The error can be ignored here because RDBMSs accept large numbers
|
|
339
|
-
# in condition clauses.
|
|
340
|
-
plain_value = predicate.right.value
|
|
341
|
-
predicate.right = plain_value
|
|
342
|
-
end
|
|
343
|
-
|
|
344
414
|
predicate
|
|
345
415
|
}.reduce(combinator_method)
|
|
346
416
|
end
|
|
@@ -368,26 +438,11 @@ module Ransack
|
|
|
368
438
|
arel_values = arel_values.reject { |v| v == ''.freeze }
|
|
369
439
|
end
|
|
370
440
|
|
|
371
|
-
# For LIKE predicates, wrap the value in Arel::Nodes.build_quoted to prevent
|
|
372
|
-
# ActiveRecord normalization from affecting wildcard patterns, and pass
|
|
373
|
-
# the escape character so the escaping done by `escape_wildcards` is
|
|
374
|
-
# actually honoured. Without an explicit ESCAPE clause, SQLite treats a
|
|
375
|
-
# backslash as a literal character rather than an escape.
|
|
376
|
-
# See https://github.com/activerecord-hackery/ransack/issues/1581
|
|
377
441
|
# A length predicate compares LENGTH(column) rather than the column.
|
|
378
442
|
attr_value = length_predicate? ? length_function_for_attribute(attribute) : attr_value_for_attribute(attribute)
|
|
379
443
|
|
|
380
444
|
if like_predicate?(arel_pred)
|
|
381
|
-
|
|
382
|
-
# iterate over an Array of patterns, so each element is quoted on its
|
|
383
|
-
# own; wrapping the whole Array in one node would hand them a single
|
|
384
|
-
# Quoted to iterate over.
|
|
385
|
-
arel_values = if arel_values.is_a?(Array)
|
|
386
|
-
arel_values.map { |v| Arel::Nodes.build_quoted(v) }
|
|
387
|
-
else
|
|
388
|
-
Arel::Nodes.build_quoted(arel_values)
|
|
389
|
-
end
|
|
390
|
-
predicate = attr_value.public_send(arel_pred, arel_values, Constants::LIKE_ESCAPE_CHARACTER)
|
|
445
|
+
predicate = like_predicate(attribute, arel_pred, arel_values)
|
|
391
446
|
else
|
|
392
447
|
predicate = attr_value.public_send(arel_pred, arel_values)
|
|
393
448
|
end
|
|
@@ -398,9 +453,39 @@ module Ransack
|
|
|
398
453
|
end
|
|
399
454
|
end
|
|
400
455
|
|
|
456
|
+
predicate = null_sentinel_requested? ? apply_null_sentinel(predicate, attr_value) : predicate
|
|
457
|
+
|
|
458
|
+
# Replace an oversized Casted integer value with the plain integer,
|
|
459
|
+
# in order to avoid ActiveModel::RangeError from Arel::Node::Casted.
|
|
460
|
+
# The error can be ignored here because RDBMSs accept large numbers
|
|
461
|
+
# in condition clauses. Applied recursively rather than only to the
|
|
462
|
+
# top-level node, because the Casted value can sit inside an In /
|
|
463
|
+
# NotIn node's `right` Array, or inside the Grouping / And / Or
|
|
464
|
+
# that Arel's own `_any` / `_all` predicates build. Done here rather
|
|
465
|
+
# than in #arel_predicate because the negated collection branch there
|
|
466
|
+
# renders this node to SQL for its correlated subquery straight away,
|
|
467
|
+
# which is where the Casted value would raise. Runs last so it also
|
|
468
|
+
# covers the OR grouping the null sentinel may have added.
|
|
469
|
+
unwrap_oversized_integers(predicate)
|
|
470
|
+
|
|
401
471
|
predicate
|
|
402
472
|
end
|
|
403
473
|
|
|
474
|
+
# `validated_values` has already excluded the sentinel above, so
|
|
475
|
+
# `predicate` here is built from the real values alone (or, when
|
|
476
|
+
# the sentinel was the only value submitted, from an empty
|
|
477
|
+
# array). An empty IN or eq_any/in_any is not `IS NULL` on its
|
|
478
|
+
# own terms (an empty `IN` is a literal false, and an empty
|
|
479
|
+
# `eq_any` builds a broken Grouping), so that case is replaced
|
|
480
|
+
# outright rather than OR'd.
|
|
481
|
+
def apply_null_sentinel(predicate, attr_value)
|
|
482
|
+
if validated_values.empty?
|
|
483
|
+
attr_value.eq(nil)
|
|
484
|
+
else
|
|
485
|
+
predicate.or(attr_value.eq(nil))
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
404
489
|
def in_predicate?(predicate)
|
|
405
490
|
return unless defined?(Arel::Nodes::Casted)
|
|
406
491
|
predicate.class == Arel::Nodes::In || predicate.class == Arel::Nodes::NotIn
|
|
@@ -415,6 +500,44 @@ module Ransack
|
|
|
415
500
|
LIKE_PREDICATES.include?(arel_predicate)
|
|
416
501
|
end
|
|
417
502
|
|
|
503
|
+
# Builds a LIKE / NOT LIKE node (or the _any / _all forms).
|
|
504
|
+
#
|
|
505
|
+
# The value is wrapped in Arel::Nodes.build_quoted so that Active
|
|
506
|
+
# Record's `normalizes` cannot rewrite the wildcards, and the escape
|
|
507
|
+
# character is passed so the escaping done by `escape_wildcards` is
|
|
508
|
+
# honoured: without an explicit ESCAPE clause SQLite treats a backslash
|
|
509
|
+
# as a literal character (#1581). The compound forms iterate over an
|
|
510
|
+
# Array of patterns, so each element is quoted on its own.
|
|
511
|
+
#
|
|
512
|
+
# Only the `i_` predicates ask for a case-insensitive match. Arel's
|
|
513
|
+
# PostgreSQL visitor renders a case-sensitive `matches` as LIKE and the
|
|
514
|
+
# other kind as ILIKE; before 6.0 the flag was never passed and every
|
|
515
|
+
# LIKE on PostgreSQL became ILIKE (#1421).
|
|
516
|
+
def like_predicate(attribute, arel_predicate, values = formatted_values_for_attribute(attribute))
|
|
517
|
+
attr_value = attr_value_for_attribute(attribute)
|
|
518
|
+
escape = Constants::LIKE_ESCAPE_CHARACTER
|
|
519
|
+
case_sensitive = !predicate.case_insensitive
|
|
520
|
+
|
|
521
|
+
case arel_predicate
|
|
522
|
+
# Arel's does_not_match_any / _all take no case_sensitive argument,
|
|
523
|
+
# unlike the other four, so they are assembled here the same way Arel
|
|
524
|
+
# assembles them.
|
|
525
|
+
when 'does_not_match_any'.freeze
|
|
526
|
+
nodes = values.map { |v| attr_value.does_not_match(Arel::Nodes.build_quoted(v), escape, case_sensitive) }
|
|
527
|
+
Arel::Nodes::Grouping.new(nodes.inject(&:or))
|
|
528
|
+
when 'does_not_match_all'.freeze
|
|
529
|
+
nodes = values.map { |v| attr_value.does_not_match(Arel::Nodes.build_quoted(v), escape, case_sensitive) }
|
|
530
|
+
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(nodes))
|
|
531
|
+
else
|
|
532
|
+
quoted = if values.is_a?(Array)
|
|
533
|
+
values.map { |v| Arel::Nodes.build_quoted(v) }
|
|
534
|
+
else
|
|
535
|
+
Arel::Nodes.build_quoted(values)
|
|
536
|
+
end
|
|
537
|
+
attr_value.public_send(arel_predicate, quoted, escape, case_sensitive)
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
418
541
|
STRING_LIKE_TYPES = %i[string text citext].freeze
|
|
419
542
|
|
|
420
543
|
def string_like_attribute?(attribute)
|
|
@@ -434,35 +557,54 @@ module Ransack
|
|
|
434
557
|
end
|
|
435
558
|
end
|
|
436
559
|
|
|
437
|
-
|
|
438
|
-
|
|
560
|
+
# Walks a predicate node for the shapes `#arel_predicate` can produce
|
|
561
|
+
# (a single Binary, an In/NotIn holding an Array, or several of these
|
|
562
|
+
# combined with Grouping/And/Or) and replaces each integer Casted
|
|
563
|
+
# value it finds with the plain integer, in place.
|
|
564
|
+
def unwrap_oversized_integers(node)
|
|
565
|
+
case node
|
|
566
|
+
when Arel::Nodes::Grouping
|
|
567
|
+
unwrap_oversized_integers(node.expr)
|
|
568
|
+
when Arel::Nodes::And, Arel::Nodes::Or
|
|
569
|
+
# Both are Nary: `grouping_any` (behind every `_any` predicate)
|
|
570
|
+
# builds a single Or with one child per value, not a pairwise
|
|
571
|
+
# chain, so `left`/`right` alone would miss the third value on.
|
|
572
|
+
node.children.each { |child| unwrap_oversized_integers(child) }
|
|
573
|
+
when Arel::Nodes::In, Arel::Nodes::NotIn
|
|
574
|
+
if node.right.is_a?(Array)
|
|
575
|
+
node.right = node.right.map { |v| plain_integer(v) || v }
|
|
576
|
+
else
|
|
577
|
+
# In / NotIn are Binary subclasses and match before that arm, so a
|
|
578
|
+
# bare Casted right (not an Array) needs the same handling as one.
|
|
579
|
+
unwrap_binary_right(node)
|
|
580
|
+
end
|
|
581
|
+
when Arel::Nodes::Binary
|
|
582
|
+
unwrap_binary_right(node)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
439
585
|
|
|
440
|
-
|
|
441
|
-
|
|
586
|
+
def unwrap_binary_right(node)
|
|
587
|
+
plain_value = plain_integer(node.right)
|
|
588
|
+
node.right = plain_value if plain_value
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def plain_integer(arel_node)
|
|
592
|
+
return nil unless arel_node.is_a?(Arel::Nodes::Casted)
|
|
442
593
|
|
|
443
594
|
relation, name = arel_node.attribute.values
|
|
444
595
|
attribute_type = relation.type_for_attribute(name).type
|
|
445
|
-
attribute_type == :integer && arel_node.value.is_a?(Integer)
|
|
596
|
+
return nil unless attribute_type == :integer && arel_node.value.is_a?(Integer)
|
|
597
|
+
|
|
598
|
+
arel_node.value
|
|
446
599
|
end
|
|
447
600
|
|
|
448
601
|
def length_predicate?
|
|
449
602
|
predicate_name.to_s.start_with?('length_')
|
|
450
603
|
end
|
|
451
604
|
|
|
452
|
-
# CHAR_LENGTH counts characters and is the SQL standard spelling; SQLite
|
|
453
|
-
# has no CHAR_LENGTH and its LENGTH already counts characters for text.
|
|
454
|
-
CHAR_LENGTH_ADAPTERS = %w[PostgreSQL PostGIS Mysql2 Trilogy].freeze
|
|
455
|
-
|
|
456
605
|
def length_function_for_attribute(attribute)
|
|
457
|
-
function_name =
|
|
458
|
-
if CHAR_LENGTH_ADAPTERS.include?(ActiveRecord::Base.adapter_class::ADAPTER_NAME)
|
|
459
|
-
'CHAR_LENGTH'.freeze
|
|
460
|
-
else
|
|
461
|
-
'LENGTH'.freeze
|
|
462
|
-
end
|
|
463
|
-
|
|
464
606
|
Arel::Nodes::NamedFunction.new(
|
|
465
|
-
|
|
607
|
+
context.dialect.length_function,
|
|
466
608
|
[attr_value_for_attribute(attribute)]
|
|
467
609
|
)
|
|
468
610
|
end
|
|
@@ -38,15 +38,9 @@ module Ransack
|
|
|
38
38
|
def conditions=(conditions)
|
|
39
39
|
case conditions
|
|
40
40
|
when Array
|
|
41
|
-
conditions.each
|
|
42
|
-
condition = Condition.new(@context).build(attrs)
|
|
43
|
-
self.conditions << condition if condition.valid?
|
|
44
|
-
end
|
|
41
|
+
conditions.each { |attrs| add_condition(attrs) }
|
|
45
42
|
when Hash
|
|
46
|
-
conditions.
|
|
47
|
-
condition = Condition.new(@context).build(attrs)
|
|
48
|
-
self.conditions << condition if condition.valid?
|
|
49
|
-
end
|
|
43
|
+
conditions.each_value { |attrs| add_condition(attrs) }
|
|
50
44
|
end
|
|
51
45
|
remove_duplicate_conditions!
|
|
52
46
|
end
|
|
@@ -126,7 +120,9 @@ module Ransack
|
|
|
126
120
|
end
|
|
127
121
|
|
|
128
122
|
def attribute_method?(name)
|
|
129
|
-
|
|
123
|
+
return false unless @context.key_within_depth_limit?(name)
|
|
124
|
+
|
|
125
|
+
stripped_name = @context.resolve_aliases(strip_predicate_and_index(name))
|
|
130
126
|
return true if @context.attribute_method?(stripped_name) ||
|
|
131
127
|
@context.attribute_method?(name)
|
|
132
128
|
case stripped_name
|
|
@@ -192,7 +188,7 @@ module Ransack
|
|
|
192
188
|
if self[name].nil?
|
|
193
189
|
stripped_name = name.dup
|
|
194
190
|
predicate = Predicate.detect_and_strip_from_string!(stripped_name)
|
|
195
|
-
aliased_attribute = context.
|
|
191
|
+
aliased_attribute = context.resolve_aliases(stripped_name)
|
|
196
192
|
name = "#{aliased_attribute}_#{predicate}" unless aliased_attribute == stripped_name
|
|
197
193
|
end
|
|
198
194
|
|
|
@@ -203,12 +199,40 @@ module Ransack
|
|
|
203
199
|
end
|
|
204
200
|
end
|
|
205
201
|
|
|
202
|
+
# Drops a multiparameter index (`created_at(1i)`) and then the predicate
|
|
203
|
+
# from a key. The index is found by a plain scan. The `/(.+?)\(/` this
|
|
204
|
+
# replaces let its lazy group re-run from every position of a long key,
|
|
205
|
+
# which CodeQL reports as polynomial (rb/polynomial-redos); Ruby's engine
|
|
206
|
+
# short-circuits that in practice, but one pass needs no such help.
|
|
206
207
|
def strip_predicate_and_index(str)
|
|
207
|
-
|
|
208
|
+
paren = str.index('(')
|
|
209
|
+
string = paren&.positive? ? str[0...paren] : str.dup
|
|
208
210
|
Predicate.detect_and_strip_from_string!(string)
|
|
209
211
|
string
|
|
210
212
|
end
|
|
211
213
|
|
|
214
|
+
# A condition is dropped when it fails validation, as an unknown
|
|
215
|
+
# attribute or predicate would be. One failure is worth naming: a
|
|
216
|
+
# single-value predicate given several values. A strict search raises
|
|
217
|
+
# for it, so the caller learns why the filter vanished (#1724). The
|
|
218
|
+
# check only applies once the predicate and an attribute have resolved;
|
|
219
|
+
# a condition that is invalid for another reason keeps its behaviour.
|
|
220
|
+
def add_condition(attrs)
|
|
221
|
+
condition = Condition.new(@context).build(attrs)
|
|
222
|
+
if condition.valid?
|
|
223
|
+
self.conditions << condition
|
|
224
|
+
elsif @context.strict_conditions? && arity_failure?(condition)
|
|
225
|
+
raise InvalidSearchError,
|
|
226
|
+
"Predicate #{condition.predicate_name} takes a single value, " \
|
|
227
|
+
"#{condition.values.size} given for #{condition.attributes.map(&:name).join(', ')}"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def arity_failure?(condition)
|
|
232
|
+
condition.predicate && condition.attributes.any?(&:valid?) &&
|
|
233
|
+
!condition.valid_arity?
|
|
234
|
+
end
|
|
235
|
+
|
|
212
236
|
def remove_duplicate_conditions!
|
|
213
237
|
# If self.conditions.uniq! is called without passing a block, then
|
|
214
238
|
# conditions differing only by ransacker_args within attributes are
|
data/lib/ransack/nodes/sort.rb
CHANGED
data/lib/ransack/nodes/value.rb
CHANGED
|
@@ -61,7 +61,14 @@ module Ransack
|
|
|
61
61
|
Time.zone.local(*val) rescue nil
|
|
62
62
|
else
|
|
63
63
|
unless val.acts_like?(:time)
|
|
64
|
-
val =
|
|
64
|
+
val = case val
|
|
65
|
+
when String then Time.zone.parse(val)
|
|
66
|
+
# A Date means midnight in Time.zone. Date#to_time would
|
|
67
|
+
# give midnight in the process's system zone, which then
|
|
68
|
+
# shifts by the offset between the two (#1436).
|
|
69
|
+
when Date then val.in_time_zone
|
|
70
|
+
else val.to_time
|
|
71
|
+
end rescue val
|
|
65
72
|
end
|
|
66
73
|
val.in_time_zone rescue nil
|
|
67
74
|
end
|
data/lib/ransack/predicate.rb
CHANGED
|
@@ -73,12 +73,19 @@ module Ransack
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
def validate(vals, type = @type)
|
|
76
|
+
def validate(vals, type = @type, sentinel = Ransack.options[:null_sentinel])
|
|
77
77
|
# An explicitly empty array is a meaningful filter (matching nothing)
|
|
78
78
|
# rather than an absent one, but only when blank values are not ignored.
|
|
79
79
|
return true if vals.empty? && wants_array &&
|
|
80
80
|
!Ransack.options[:ignore_blank_values]
|
|
81
81
|
|
|
82
|
+
# The null sentinel is a marker compared by identity, never cast to
|
|
83
|
+
# the column's type. Casting it here would be the only place it is
|
|
84
|
+
# cast at all: a cast to :date or :integer turns most strings into
|
|
85
|
+
# nil, which the validator then rejects, dropping the whole condition
|
|
86
|
+
# when the sentinel is the only value submitted (#940).
|
|
87
|
+
return true if Constants.null_sentinel_requested?(name, vals, sentinel)
|
|
88
|
+
|
|
82
89
|
# When blank values are meaningful, validate the value as given. Casting
|
|
83
90
|
# first would turn '' into nil for an integer or boolean column and the
|
|
84
91
|
# explicit blank would be dropped — leaving no condition at all.
|