ransack 6.0.1 → 6.1.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -68
  3. data/lib/ransack/active_record/context.rb +207 -75
  4. data/lib/ransack/active_record/join_association.rb +8 -17
  5. data/lib/ransack/active_record/join_dependency.rb +163 -9
  6. data/lib/ransack/configuration.rb +25 -2
  7. data/lib/ransack/context.rb +55 -7
  8. data/lib/ransack/errors.rb +13 -0
  9. data/lib/ransack/invalid_search_error.rb +3 -3
  10. data/lib/ransack/locale/ar.yml +35 -0
  11. data/lib/ransack/locale/az.yml +36 -1
  12. data/lib/ransack/locale/bg.yml +35 -0
  13. data/lib/ransack/locale/ca.yml +36 -1
  14. data/lib/ransack/locale/cs.yml +35 -0
  15. data/lib/ransack/locale/da.yml +35 -0
  16. data/lib/ransack/locale/de.yml +36 -1
  17. data/lib/ransack/locale/el.yml +36 -1
  18. data/lib/ransack/locale/en.yml +35 -0
  19. data/lib/ransack/locale/es.yml +36 -1
  20. data/lib/ransack/locale/fa.yml +35 -0
  21. data/lib/ransack/locale/fi.yml +35 -0
  22. data/lib/ransack/locale/fr.yml +35 -0
  23. data/lib/ransack/locale/hu.yml +35 -0
  24. data/lib/ransack/locale/id.yml +35 -0
  25. data/lib/ransack/locale/it.yml +35 -0
  26. data/lib/ransack/locale/ja.yml +35 -0
  27. data/lib/ransack/locale/ko.yml +35 -0
  28. data/lib/ransack/locale/nl.yml +35 -0
  29. data/lib/ransack/locale/pt-BR.yml +35 -0
  30. data/lib/ransack/locale/ro.yml +35 -0
  31. data/lib/ransack/locale/ru.yml +35 -0
  32. data/lib/ransack/locale/sk.yml +35 -0
  33. data/lib/ransack/locale/sv.yml +35 -0
  34. data/lib/ransack/locale/tr.yml +36 -1
  35. data/lib/ransack/locale/uk.yml +35 -0
  36. data/lib/ransack/locale/zh-CN.yml +35 -0
  37. data/lib/ransack/locale/zh-TW.yml +35 -0
  38. data/lib/ransack/nodes/condition.rb +168 -42
  39. data/lib/ransack/nodes/grouping.rb +8 -17
  40. data/lib/ransack/nodes/node.rb +23 -1
  41. data/lib/ransack/nodes/sort.rb +7 -4
  42. data/lib/ransack/nodes/value.rb +6 -0
  43. data/lib/ransack/predicate.rb +19 -6
  44. data/lib/ransack/search.rb +21 -10
  45. data/lib/ransack/translate.rb +43 -35
  46. data/lib/ransack/version.rb +1 -1
  47. data/lib/ransack.rb +2 -1
  48. metadata +7 -6
@@ -32,7 +32,7 @@ tr:
32
32
  lteq: "daha küçük veya eşit"
33
33
  lteq_any: "daha küçük veya herhangi birine eşit"
34
34
  lteq_all: "daha küçük veya hepsine eşit"
35
- gt: "daha büyük "
35
+ gt: "daha büyük"
36
36
  gt_any: "herhangi birinden daha büyük"
37
37
  gt_all: "hepsinden daha büyük"
38
38
  gteq: "daha büyük veya eşit"
@@ -50,21 +50,56 @@ tr:
50
50
  not_cont: "içermeyen"
51
51
  not_cont_any: "herhangi birini içermeyen"
52
52
  not_cont_all: "hiçbirini birini içermeyen"
53
+ i_cont: "içeren, büyük-küçük harf ayrımı olmadan"
54
+ i_cont_any: "herhangi birini içeren, büyük-küçük harf ayrımı olmadan"
55
+ i_cont_all: "hepsini içeren, büyük-küçük harf ayrımı olmadan"
56
+ not_i_cont: "içermeyen, büyük-küçük harf ayrımı olmadan"
57
+ not_i_cont_any: "herhangi birini içermeyen, büyük-küçük harf ayrımı olmadan"
58
+ not_i_cont_all: "hiçbirini birini içermeyen, büyük-küçük harf ayrımı olmadan"
53
59
  start: "ile başlayan"
54
60
  start_any: "herhangi biriyle başlayan"
55
61
  start_all: "hepsiyle başlayan"
56
62
  not_start: "ile başlamayan"
57
63
  not_start_any: "herhangi biriyle başlamayan"
58
64
  not_start_all: "hiçbiriyle başlamayan"
65
+ i_start: "ile başlayan, büyük-küçük harf ayrımı olmadan"
66
+ i_start_any: "herhangi biriyle başlayan, büyük-küçük harf ayrımı olmadan"
67
+ i_start_all: "hepsiyle başlayan, büyük-küçük harf ayrımı olmadan"
68
+ not_i_start: "ile başlamayan, büyük-küçük harf ayrımı olmadan"
69
+ not_i_start_any: "herhangi biriyle başlamayan, büyük-küçük harf ayrımı olmadan"
70
+ not_i_start_all: "hiçbiriyle başlamayan, büyük-küçük harf ayrımı olmadan"
59
71
  end: "ile biten"
60
72
  end_any: "herhangi biriyle biten"
61
73
  end_all: "hepsi ile biten"
62
74
  not_end: "ile bitmeyen"
63
75
  not_end_any: "herhangi biriyle bitmeyen"
64
76
  not_end_all: "hiçbiriyle bitmeyen"
77
+ i_end: "ile biten, büyük-küçük harf ayrımı olmadan"
78
+ i_end_any: "herhangi biriyle biten, büyük-küçük harf ayrımı olmadan"
79
+ i_end_all: "hepsi ile biten, büyük-küçük harf ayrımı olmadan"
80
+ not_i_end: "ile bitmeyen, büyük-küçük harf ayrımı olmadan"
81
+ not_i_end_any: "herhangi biriyle bitmeyen, büyük-küçük harf ayrımı olmadan"
82
+ not_i_end_all: "hiçbiriyle bitmeyen, büyük-küçük harf ayrımı olmadan"
65
83
  'true': "doğru"
84
+ not_true: "doğru değil"
66
85
  'false': "yanlış"
86
+ not_false: "yanlış değil"
67
87
  present: "mevcut"
68
88
  blank: "boş"
69
89
  'null': "geçersiz"
70
90
  not_null: "geçerli"
91
+ length_eq: "uzunluğu eşit"
92
+ length_eq_any: "uzunluğu herhangi birine eşit"
93
+ length_eq_all: "uzunluğu hepsine eşit"
94
+ length_lt: "uzunluğu daha küçük"
95
+ length_lt_any: "uzunluğu herhangi birinden küçük"
96
+ length_lt_all: "uzunluğu hepsinden küçük"
97
+ length_lteq: "uzunluğu daha küçük veya eşit"
98
+ length_lteq_any: "uzunluğu daha küçük veya herhangi birine eşit"
99
+ length_lteq_all: "uzunluğu daha küçük veya hepsine eşit"
100
+ length_gt: "uzunluğu daha büyük"
101
+ length_gt_any: "uzunluğu herhangi birinden daha büyük"
102
+ length_gt_all: "uzunluğu hepsinden daha büyük"
103
+ length_gteq: "uzunluğu daha büyük veya eşit"
104
+ length_gteq_any: "uzunluğu daha büyük veya herhangi birine eşit"
105
+ length_gteq_all: "uzunluğu daha büyük veya hepsine eşit"
@@ -52,21 +52,56 @@ uk:
52
52
  not_cont: не містить
53
53
  not_cont_any: не містить жодного
54
54
  not_cont_all: не містить усіх
55
+ i_cont: містить, без урахування регістру
56
+ i_cont_any: містить будь-який, без урахування регістру
57
+ i_cont_all: містить усі, без урахування регістру
58
+ not_i_cont: не містить, без урахування регістру
59
+ not_i_cont_any: не містить жодного, без урахування регістру
60
+ not_i_cont_all: не містить усіх, без урахування регістру
55
61
  start: починається з
56
62
  start_any: починається з будь-якого
57
63
  start_all: починається з усіх
58
64
  not_start: не починається з
59
65
  not_start_any: не починається з будь-якого
60
66
  not_start_all: не починається з усіх
67
+ i_start: починається з, без урахування регістру
68
+ i_start_any: починається з будь-якого, без урахування регістру
69
+ i_start_all: починається з усіх, без урахування регістру
70
+ not_i_start: не починається з, без урахування регістру
71
+ not_i_start_any: не починається з будь-якого, без урахування регістру
72
+ not_i_start_all: не починається з усіх, без урахування регістру
61
73
  end: закінчується на
62
74
  end_any: закінчується на будь-який
63
75
  end_all: закінчується на всі
64
76
  not_end: не закінчується на
65
77
  not_end_any: не закінчується на будь-який
66
78
  not_end_all: не закінчується на всі
79
+ i_end: закінчується на, без урахування регістру
80
+ i_end_any: закінчується на будь-який, без урахування регістру
81
+ i_end_all: закінчується на всі, без урахування регістру
82
+ not_i_end: не закінчується на, без урахування регістру
83
+ not_i_end_any: не закінчується на будь-який, без урахування регістру
84
+ not_i_end_all: не закінчується на всі, без урахування регістру
67
85
  'true': так
86
+ not_true: не істина
68
87
  'false': ні
88
+ not_false: не хиба
69
89
  present: присутній
70
90
  blank: порожній
71
91
  'null': нульовий
72
92
  not_null: не нульовий
93
+ length_eq: довжина рівна
94
+ length_eq_any: довжина рівна будь-якому
95
+ length_eq_all: довжина рівна усім
96
+ length_lt: довжина менше ніж
97
+ length_lt_any: довжина менше за будь-який
98
+ length_lt_all: довжина менше за всі
99
+ length_lteq: довжина менше або рівна
100
+ length_lteq_any: довжина менше або рівна будь-якому
101
+ length_lteq_all: довжина менше або рівна всім
102
+ length_gt: довжина більше ніж
103
+ length_gt_any: довжина більше ніж будь-який
104
+ length_gt_all: довжина більше ніж усі
105
+ length_gteq: довжина більше або рівна
106
+ length_gteq_any: довжина більше або рівна будь-якому
107
+ length_gteq_all: довжина більше або рівна всім
@@ -50,21 +50,56 @@ zh-CN:
50
50
  not_cont: "不包含"
51
51
  not_cont_any: "不包含任意一个值"
52
52
  not_cont_all: "不包含所有值"
53
+ i_cont: "包含(忽略大小写)"
54
+ i_cont_any: "包含任意一个值(忽略大小写)"
55
+ i_cont_all: "包含所有值(忽略大小写)"
56
+ not_i_cont: "不包含(忽略大小写)"
57
+ not_i_cont_any: "不包含任意一个值(忽略大小写)"
58
+ not_i_cont_all: "不包含所有值(忽略大小写)"
53
59
  start: "始于"
54
60
  start_any: "始于任一值"
55
61
  start_all: "始于任意值"
56
62
  not_start: "非始于"
57
63
  not_start_any: "非始于任一值"
58
64
  not_start_all: "非始于任意值"
65
+ i_start: "始于(忽略大小写)"
66
+ i_start_any: "始于任一值(忽略大小写)"
67
+ i_start_all: "始于任意值(忽略大小写)"
68
+ not_i_start: "非始于(忽略大小写)"
69
+ not_i_start_any: "非始于任一值(忽略大小写)"
70
+ not_i_start_all: "非始于任意值(忽略大小写)"
59
71
  end: "止于"
60
72
  end_any: "止于任一值"
61
73
  end_all: "止于任意值"
62
74
  not_end: "非止于"
63
75
  not_end_any: "非止于任一值"
64
76
  not_end_all: "非止于任意值"
77
+ i_end: "止于(忽略大小写)"
78
+ i_end_any: "止于任一值(忽略大小写)"
79
+ i_end_all: "止于任意值(忽略大小写)"
80
+ not_i_end: "非止于(忽略大小写)"
81
+ not_i_end_any: "非止于任一值(忽略大小写)"
82
+ not_i_end_all: "非止于任意值(忽略大小写)"
65
83
  'true': "等于true"
84
+ not_true: "不等于true"
66
85
  'false': "等于false"
86
+ not_false: "不等于false"
67
87
  present: "有值"
68
88
  blank: "为空"
69
89
  'null': "是null"
70
90
  not_null: "不是null"
91
+ length_eq: "长度等于"
92
+ length_eq_any: "长度等于任意值"
93
+ length_eq_all: "长度等于所有值"
94
+ length_lt: "长度小于"
95
+ length_lt_any: "长度小于任意一个值"
96
+ length_lt_all: "长度小于所有值"
97
+ length_lteq: "长度小于等于"
98
+ length_lteq_any: "长度小于等于任意一个值"
99
+ length_lteq_all: "长度小于等于所有值"
100
+ length_gt: "长度大于"
101
+ length_gt_any: "长度大于任意一个值"
102
+ length_gt_all: "长度大于所有值"
103
+ length_gteq: "长度大于等于"
104
+ length_gteq_any: "长度大于等于任意一个值"
105
+ length_gteq_all: "长度大于等于所有值"
@@ -50,21 +50,56 @@ zh-TW:
50
50
  not_cont: "不包含"
51
51
  not_cont_any: "不包含任何一個值"
52
52
  not_cont_all: "不包含所有值"
53
+ i_cont: "包含(忽略大小寫)"
54
+ i_cont_any: "包含任何一個值(忽略大小寫)"
55
+ i_cont_all: "包含所有值(忽略大小寫)"
56
+ not_i_cont: "不包含(忽略大小寫)"
57
+ not_i_cont_any: "不包含任何一個值(忽略大小寫)"
58
+ not_i_cont_all: "不包含所有值(忽略大小寫)"
53
59
  start: "以某個值開始"
54
60
  start_any: "以任何一個值開始"
55
61
  start_all: "以所有值開始"
56
62
  not_start: "不以某個值開始"
57
63
  not_start_any: "不以任何一值開始"
58
64
  not_start_all: "不以所有值開始"
65
+ i_start: "以某個值開始(忽略大小寫)"
66
+ i_start_any: "以任何一個值開始(忽略大小寫)"
67
+ i_start_all: "以所有值開始(忽略大小寫)"
68
+ not_i_start: "不以某個值開始(忽略大小寫)"
69
+ not_i_start_any: "不以任何一值開始(忽略大小寫)"
70
+ not_i_start_all: "不以所有值開始(忽略大小寫)"
59
71
  end: "以某個值結尾"
60
72
  end_any: "以任何一個值結尾"
61
73
  end_all: "以所有值結尾"
62
74
  not_end: "不以某個值結尾"
63
75
  not_end_any: "不以任何一個值結尾"
64
76
  not_end_all: "不以所有值結尾"
77
+ i_end: "以某個值結尾(忽略大小寫)"
78
+ i_end_any: "以任何一個值結尾(忽略大小寫)"
79
+ i_end_all: "以所有值結尾(忽略大小寫)"
80
+ not_i_end: "不以某個值結尾(忽略大小寫)"
81
+ not_i_end_any: "不以任何一個值結尾(忽略大小寫)"
82
+ not_i_end_all: "不以所有值結尾(忽略大小寫)"
65
83
  'true': "為真"
84
+ not_true: "不為真"
66
85
  'false': "為假"
86
+ not_false: "不為假"
67
87
  present: "有值"
68
88
  blank: "為空"
69
89
  'null': "為 null"
70
90
  not_null: "不為 null"
91
+ length_eq: "長度等於"
92
+ length_eq_any: "長度等於任何一個值"
93
+ length_eq_all: "長度等於所有值"
94
+ length_lt: "長度小於"
95
+ length_lt_any: "長度小於任何一個值"
96
+ length_lt_all: "長度小於所有值"
97
+ length_lteq: "長度小於或等於"
98
+ length_lteq_any: "長度小於或等於任何一個值"
99
+ length_lteq_all: "長度小於或等於所有值"
100
+ length_gt: "長度大於"
101
+ length_gt_any: "長度大於任何一個值"
102
+ length_gt_all: "長度大於所有值"
103
+ length_gteq: "長度大於或等於"
104
+ length_gteq_any: "長度大於或等於任何一個值"
105
+ length_gteq_all: "長度大於或等於所有值"
@@ -1,4 +1,4 @@
1
- require 'ransack/invalid_search_error'
1
+ require 'ransack/errors'
2
2
 
3
3
  module Ransack
4
4
  module Nodes
@@ -34,9 +34,15 @@ module Ransack
34
34
  m: combinator,
35
35
  v: predicate.wants_array ? Array(values) : [values]
36
36
  )
37
+ # An Array given to a single-value predicate is spread into
38
+ # several values by #build, so it fails the arity check here the
39
+ # same way the c: form does (#1772).
40
+ if condition.arity_failure?
41
+ raise condition.arity_error if context&.strict_conditions?
42
+ nil
37
43
  # TODO: Figure out what to do with multiple types of attributes,
38
44
  # if anything. Tempted to go with "garbage in, garbage out" here.
39
- if predicate.validate(condition.values, condition.default_type, condition.null_sentinel)
45
+ elsif predicate.validate(condition.values, condition.default_type, condition.null_sentinel)
40
46
  condition
41
47
  else
42
48
  nil
@@ -97,6 +103,21 @@ module Ransack
97
103
  values.size <= 1 || predicate.wants_array
98
104
  end
99
105
 
106
+ # A single-value predicate given several values is worth naming, so a
107
+ # strict search can say why the condition was dropped (#1724). It only
108
+ # counts once the predicate and an attribute have resolved; a condition
109
+ # that is invalid for another reason is not an arity failure.
110
+ def arity_failure?
111
+ predicate && attributes.any?(&:valid?) && !valid_arity?
112
+ end
113
+
114
+ def arity_error
115
+ InvalidSearchError.new(
116
+ "Predicate #{predicate_name} takes a single value, " \
117
+ "#{values.size} given for #{attributes.map(&:name).join(', ')}"
118
+ )
119
+ end
120
+
100
121
  def attributes
101
122
  @attributes ||= []
102
123
  end
@@ -115,10 +136,11 @@ module Ransack
115
136
  end
116
137
  when Hash
117
138
  args.each do |index, attrs|
139
+ attrs = indifferent_params(attrs, 'attribute')
118
140
  build_attributes(attrs[:name], attrs[:ransacker_args])
119
141
  end
120
142
  else
121
- raise ArgumentError,
143
+ raise InvalidSearchError,
122
144
  "Invalid argument (#{args.class}) supplied to attributes="
123
145
  end
124
146
  end
@@ -140,11 +162,11 @@ module Ransack
140
162
  end
141
163
  when Hash
142
164
  args.each do |index, attrs|
143
- val = Value.new(@context, attrs[:value])
165
+ val = Value.new(@context, indifferent_params(attrs, 'value')[:value])
144
166
  self.values << val
145
167
  end
146
168
  else
147
- raise ArgumentError,
169
+ raise InvalidSearchError,
148
170
  "Invalid argument (#{args.class}) supplied to values="
149
171
  end
150
172
  end
@@ -196,6 +218,7 @@ module Ransack
196
218
  # compound's combinator (#1728). Left alone, the alias passed `valid?`
197
219
  # by being allowlisted and bound a column that does not exist.
198
220
  def build_attributes(name, ransacker_args = [])
221
+ scalar_param(name, 'an attribute name')
199
222
  return build_attribute(name, ransacker_args) if name.blank? || @context.nil?
200
223
 
201
224
  resolved = @context.resolve_aliases(name.to_s)
@@ -211,9 +234,6 @@ module Ransack
211
234
  Attribute.new(@context, name, ransacker_args).tap do |attribute|
212
235
  @context.bind(attribute, attribute.name)
213
236
  self.attributes << attribute if name.nil? || attribute.valid?
214
- if predicate && !negative?
215
- @context.lock_association(attribute.parent)
216
- end
217
237
  end
218
238
  end
219
239
 
@@ -254,15 +274,45 @@ module Ransack
254
274
  }.freeze
255
275
 
256
276
  def build(params)
257
- params.with_indifferent_access.each do |key, value|
258
- if key.match(/^(a|v|p|m|attributes|values|predicate|predicate_name|combinator)$/)
277
+ indifferent_params(params, 'condition').each do |key, value|
278
+ if key.match(/\A(a|v|p|m|attributes|values|predicate|predicate_name|combinator)\z/)
259
279
  self.send("#{LONG_KEYS.fetch(key, key)}=", value)
260
280
  end
261
281
  end
282
+ spread_array_values!
262
283
 
263
284
  self
264
285
  end
265
286
 
287
+ # An Array held by one value stands for several values: `[5, 6]` in one
288
+ # envelope of the c: form (#1771), or `name_eq: ['a', 'b']` in the short
289
+ # form, as a group of check boxes posts it (#1772). Cast as one value it
290
+ # became nil on a typed column, dropping the condition, or its string
291
+ # form elsewhere. Each element becomes a value of its own instead, so a
292
+ # predicate that wants an array gets them all, and a single-value
293
+ # predicate is held to its arity like any condition given several
294
+ # values; a one-element Array is just its element. Blank elements, such
295
+ # as the empty string a check box group posts alongside its choices, are
296
+ # dropped when blank values are ignored. Date and time types keep the
297
+ # Array, which their cast builds one value from.
298
+ def spread_array_values!
299
+ return unless predicate && values.any? { |v| v.value.is_a?(Array) }
300
+ # An incomplete or unsearchable attribute has no type to look up;
301
+ # valid? discards the condition instead.
302
+ return unless predicate.type || attributes.first&.valid?
303
+ return if Value::ARRAY_CAST_TYPES.include?(default_type&.to_sym)
304
+
305
+ @values = values.flat_map do |v|
306
+ next [v] unless v.value.is_a?(Array)
307
+
308
+ elements = v.value
309
+ if Ransack.options[:ignore_blank_values]
310
+ elements = elements.reject { |e| e.blank? && e != false && !e.nil? }
311
+ end
312
+ elements.map { |e| Value.new(@context, e) }
313
+ end
314
+ end
315
+
266
316
  def persisted?
267
317
  false
268
318
  end
@@ -286,10 +336,7 @@ module Ransack
286
336
  end
287
337
 
288
338
  def predicate_name=(name)
289
- self.predicate = Predicate.named(name)
290
- if predicate && !negative?
291
- attributes.each { |a| context.lock_association(a.parent) }
292
- end
339
+ self.predicate = Predicate.named(scalar_param(name, 'a predicate'))
293
340
  @predicate
294
341
  end
295
342
  alias :p= :predicate_name=
@@ -314,7 +361,7 @@ module Ransack
314
361
  end
315
362
 
316
363
  def casted_values_for_attribute(attr)
317
- validated_values.map { |v| v.cast(predicate.type || attr.type) }
364
+ validated_values.map { |v| v.cast(value_type_for_attribute(attr)) }
318
365
  end
319
366
 
320
367
  def formatted_values_for_attribute(attr)
@@ -336,7 +383,13 @@ module Ransack
336
383
  end
337
384
 
338
385
  def arel_predicate_for_attribute(attr)
339
- if predicate.arel_predicate === Proc
386
+ # `.arel_predicate === Proc` reads as a class check, but `Proc#===`
387
+ # is an alias for `#call`. It actually invokes the proc, passing
388
+ # the `Proc` class itself as the argument, and branches on whatever
389
+ # comes back. `#respond_to?(:call)` is a real check, and it also
390
+ # catches an object that defines `#call` but is neither a Proc nor
391
+ # a Method, which `=== Proc` treats as a method name.
392
+ if predicate.arel_predicate.respond_to?(:call)
340
393
  values = casted_values_for_attribute(attr)
341
394
  unless predicate.wants_array
342
395
  values = values.first
@@ -362,7 +415,8 @@ module Ransack
362
415
  end
363
416
 
364
417
  def default_type
365
- predicate.type || (attributes.first && attributes.first.type)
418
+ attribute = attributes.first
419
+ predicate.type || (attribute && value_type_for_attribute(attribute))
366
420
  end
367
421
 
368
422
  def inspect
@@ -385,29 +439,29 @@ module Ransack
385
439
  def arel_predicate
386
440
  attributes.map { |attribute|
387
441
  association = attribute.parent
388
- parent_table = association.table
389
442
 
390
- predicate = if negative? && attribute.associated_collection? && not_nested_condition(attribute, parent_table)
391
- query = context.build_correlated_subquery(association)
392
- context.remove_association(association)
443
+ predicate = if negative? && attribute.associated_collection? && not_nested_condition(attribute)
444
+ outer_key, query = context.correlated_subquery(association)
445
+ context.use_ancestors_of(association)
393
446
 
394
447
  case self.predicate_name
395
448
  when 'not_null'
396
449
  if self.value
397
450
  query.where(format_predicate(attribute))
398
- Arel::Nodes::In.new(context.primary_key, Arel.sql(query.to_sql))
451
+ Arel::Nodes::In.new(outer_key, Arel.sql(context.subquery_sql(query)))
399
452
  else
400
453
  query.where(format_predicate(attribute).not)
401
- Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
454
+ Arel::Nodes::NotIn.new(outer_key, Arel.sql(context.subquery_sql(query)))
402
455
  end
403
456
  when 'not_cont'
404
457
  query.where(like_predicate(attribute, 'matches'.freeze))
405
- Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
458
+ Arel::Nodes::NotIn.new(outer_key, Arel.sql(context.subquery_sql(query)))
406
459
  else
407
460
  query.where(format_predicate(attribute).not)
408
- Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
461
+ Arel::Nodes::NotIn.new(outer_key, Arel.sql(context.subquery_sql(query)))
409
462
  end
410
463
  else
464
+ context.use_association(attribute.parent)
411
465
  format_predicate(attribute)
412
466
  end
413
467
 
@@ -415,8 +469,15 @@ module Ransack
415
469
  }.reduce(combinator_method)
416
470
  end
417
471
 
418
- def not_nested_condition(attribute, parent_table)
419
- parent_table.class != Arel::Nodes::TableAlias && attribute.name.starts_with?(parent_table.name)
472
+ # Whether the attribute is on a collection the searched model has
473
+ # directly, rather than one nested under another association. A
474
+ # negative condition on a nested collection keeps its join (#1411).
475
+ # Told apart by the association's depth, not by its name: the name of
476
+ # an STI, scoped, aliased or through collection need not start with
477
+ # its table's (#1773). The table argument is no longer used, and is
478
+ # kept for callers of the 4.4.0 API.
479
+ def not_nested_condition(attribute, _parent_table = nil)
480
+ context.root_association?(attribute.parent)
420
481
  end
421
482
 
422
483
  private
@@ -425,18 +486,28 @@ module Ransack
425
486
  combinator === Constants::OR ? :or : :and
426
487
  end
427
488
 
489
+ # The type a value is cast to for this attribute. A LIKE compares text,
490
+ # so its value stays text whatever the column's type: cast to an integer,
491
+ # 'Alpha' becomes 0 and the pattern '%0%' matches unrelated rows, and cast
492
+ # to a date it becomes nil, which either drops the condition or gives the
493
+ # pattern '%%' (#1793). A ransacker's formatter is still given the type
494
+ # the ransacker declares, as it may depend on it, and an Array still goes
495
+ # to the attribute's cast: a multiparameter date has to be assembled, and
496
+ # #spread_array_values! decides from this type whether to keep the Array.
497
+ def value_type_for_attribute(attr)
498
+ return predicate.type if predicate.type
499
+ return attr.type unless like_predicate?(predicate.arel_predicate)
500
+ return attr.type if attr.ransacker&.formatter
501
+ return attr.type if values.any? { |v| v.value.is_a?(Array) }
502
+
503
+ :string
504
+ end
505
+
428
506
  def format_predicate(attribute)
429
- arel_pred = arel_predicate_for_attribute(attribute)
430
- arel_values = formatted_values_for_attribute(attribute)
507
+ return format_predicate_from_node(attribute) if predicate.arel_node
431
508
 
432
- # The `present` / `blank` predicate formatters produce `[nil, '']` regardless
433
- # of column type. The empty-string half has no meaning on non-string columns:
434
- # ActiveRecord casts `''` to NULL for those types, producing the
435
- # always-UNKNOWN `column != NULL` (or `column = NULL`) clause. Strip the
436
- # empty string when the attribute is not a string-like column.
437
- if arel_values.is_a?(Array) && arel_values.include?(''.freeze) && !string_like_attribute?(attribute)
438
- arel_values = arel_values.reject { |v| v == ''.freeze }
439
- end
509
+ arel_pred = arel_predicate_for_attribute(attribute)
510
+ arel_values = formatted_values_without_blank_for_non_string(attribute)
440
511
 
441
512
  # A length predicate compares LENGTH(column) rather than the column.
442
513
  attr_value = length_predicate? ? length_function_for_attribute(attribute) : attr_value_for_attribute(attribute)
@@ -491,6 +562,57 @@ module Ransack
491
562
  predicate.class == Arel::Nodes::In || predicate.class == Arel::Nodes::NotIn
492
563
  end
493
564
 
565
+ # The `present` / `blank` predicate formatters produce `[nil, '']` regardless
566
+ # of column type. The empty-string half has no meaning on non-string columns:
567
+ # ActiveRecord casts `''` to NULL for those types, producing the
568
+ # always-UNKNOWN `column != NULL` (or `column = NULL`) clause. Strip the
569
+ # empty string when the attribute is not a string-like column. A LIKE
570
+ # keeps it: its value stays text, so `''` is a pattern like any other.
571
+ def formatted_values_without_blank_for_non_string(attribute)
572
+ arel_values = formatted_values_for_attribute(attribute)
573
+
574
+ if arel_values.is_a?(Array) && arel_values.include?(''.freeze) &&
575
+ !string_like_attribute?(attribute) && !like_predicate?(predicate.arel_predicate)
576
+ arel_values = arel_values.reject { |v| v == ''.freeze }
577
+ end
578
+
579
+ arel_values
580
+ end
581
+
582
+ # `arel_node` receives the attribute node and the already cast and
583
+ # formatted values, and returns a finished Arel node directly,
584
+ # instead of naming a method `#format_predicate` sends to
585
+ # `attr_value` with one argument.
586
+ def format_predicate_from_node(attribute)
587
+ attr_value = attr_value_for_attribute(attribute)
588
+ # A sentinel-only search has no real values, so the builder gets no
589
+ # input and the condition is `IS NULL` alone.
590
+ return attr_value.eq(nil) if null_sentinel_requested? && validated_values.empty?
591
+
592
+ values = formatted_values_without_blank_for_non_string(attribute)
593
+ predicate = self.predicate.arel_node.call(attr_value, values)
594
+
595
+ # A nil result would drop the condition and a String would reach the
596
+ # SQL as raw text, both without an error.
597
+ unless predicate.is_a?(Arel::Nodes::Node)
598
+ raise ArgumentError,
599
+ "arel_node for '#{self.predicate.name}' must return an Arel node, " \
600
+ "got #{predicate.class}"
601
+ end
602
+
603
+ if in_predicate?(predicate) && predicate.right.is_a?(Array)
604
+ predicate.right = predicate.right.map do |pr|
605
+ casted_array?(pr) ? format_values_for(pr) : pr
606
+ end
607
+ end
608
+
609
+ predicate = null_sentinel_requested? ? apply_null_sentinel(predicate, attr_value) : predicate
610
+
611
+ unwrap_oversized_integers(predicate)
612
+
613
+ predicate
614
+ end
615
+
494
616
  LIKE_PREDICATES = %w[
495
617
  matches matches_any matches_all
496
618
  does_not_match does_not_match_any does_not_match_all
@@ -557,18 +679,21 @@ module Ransack
557
679
  end
558
680
  end
559
681
 
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.
682
+ # Walks a predicate node for the shapes `#arel_predicate` or an
683
+ # `arel_node` builder can produce (a single Binary, an In/NotIn
684
+ # holding an Array, a Between, or several of these combined with
685
+ # Grouping/Not/And/Or) and replaces each integer Casted value it
686
+ # finds with the plain integer, in place.
564
687
  def unwrap_oversized_integers(node)
565
688
  case node
566
- when Arel::Nodes::Grouping
689
+ when Arel::Nodes::Grouping, Arel::Nodes::Not
567
690
  unwrap_oversized_integers(node.expr)
568
691
  when Arel::Nodes::And, Arel::Nodes::Or
569
692
  # Both are Nary: `grouping_any` (behind every `_any` predicate)
570
693
  # builds a single Or with one child per value, not a pairwise
571
694
  # chain, so `left`/`right` alone would miss the third value on.
695
+ # A BETWEEN keeps its two bounds as Casted children of an And.
696
+ node.children.map! { |child| plain_integer(child) || child }
572
697
  node.children.each { |child| unwrap_oversized_integers(child) }
573
698
  when Arel::Nodes::In, Arel::Nodes::NotIn
574
699
  if node.right.is_a?(Array)
@@ -580,6 +705,7 @@ module Ransack
580
705
  end
581
706
  when Arel::Nodes::Binary
582
707
  unwrap_binary_right(node)
708
+ unwrap_oversized_integers(node.right) if node.right.is_a?(Arel::Nodes::And)
583
709
  end
584
710
  end
585
711