mini-fast-sys 0.0.1

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 (94) hide show
  1. checksums.yaml +7 -0
  2. data/mini-fast-sys.gemspec +12 -0
  3. data/ransack-4.4.1/LICENSE +20 -0
  4. data/ransack-4.4.1/README.md +101 -0
  5. data/ransack-4.4.1/lib/polyamorous/activerecord/join_association.rb +70 -0
  6. data/ransack-4.4.1/lib/polyamorous/activerecord/join_association_7_2.rb +55 -0
  7. data/ransack-4.4.1/lib/polyamorous/activerecord/join_dependency.rb +102 -0
  8. data/ransack-4.4.1/lib/polyamorous/activerecord/reflection.rb +11 -0
  9. data/ransack-4.4.1/lib/polyamorous/join.rb +70 -0
  10. data/ransack-4.4.1/lib/polyamorous/polyamorous.rb +27 -0
  11. data/ransack-4.4.1/lib/polyamorous/swapping_reflection_class.rb +11 -0
  12. data/ransack-4.4.1/lib/polyamorous/tree_node.rb +7 -0
  13. data/ransack-4.4.1/lib/ransack/active_record.rb +14 -0
  14. data/ransack-4.4.1/lib/ransack/adapters/active_record/base.rb +152 -0
  15. data/ransack-4.4.1/lib/ransack/adapters/active_record/context.rb +373 -0
  16. data/ransack-4.4.1/lib/ransack/configuration.rb +209 -0
  17. data/ransack-4.4.1/lib/ransack/constants.rb +176 -0
  18. data/ransack-4.4.1/lib/ransack/context.rb +190 -0
  19. data/ransack-4.4.1/lib/ransack/helpers/form_builder.rb +277 -0
  20. data/ransack-4.4.1/lib/ransack/helpers/form_helper.rb +311 -0
  21. data/ransack-4.4.1/lib/ransack/helpers.rb +2 -0
  22. data/ransack-4.4.1/lib/ransack/invalid_search_error.rb +3 -0
  23. data/ransack-4.4.1/lib/ransack/locale/ar.yml +70 -0
  24. data/ransack-4.4.1/lib/ransack/locale/az.yml +70 -0
  25. data/ransack-4.4.1/lib/ransack/locale/bg.yml +70 -0
  26. data/ransack-4.4.1/lib/ransack/locale/ca.yml +70 -0
  27. data/ransack-4.4.1/lib/ransack/locale/cs.yml +70 -0
  28. data/ransack-4.4.1/lib/ransack/locale/da.yml +70 -0
  29. data/ransack-4.4.1/lib/ransack/locale/de.yml +70 -0
  30. data/ransack-4.4.1/lib/ransack/locale/el.yml +70 -0
  31. data/ransack-4.4.1/lib/ransack/locale/en.yml +70 -0
  32. data/ransack-4.4.1/lib/ransack/locale/es.yml +70 -0
  33. data/ransack-4.4.1/lib/ransack/locale/fa.yml +70 -0
  34. data/ransack-4.4.1/lib/ransack/locale/fi.yml +71 -0
  35. data/ransack-4.4.1/lib/ransack/locale/fr.yml +70 -0
  36. data/ransack-4.4.1/lib/ransack/locale/hu.yml +70 -0
  37. data/ransack-4.4.1/lib/ransack/locale/id.yml +70 -0
  38. data/ransack-4.4.1/lib/ransack/locale/it.yml +70 -0
  39. data/ransack-4.4.1/lib/ransack/locale/ja.yml +70 -0
  40. data/ransack-4.4.1/lib/ransack/locale/ko.yml +70 -0
  41. data/ransack-4.4.1/lib/ransack/locale/nl.yml +70 -0
  42. data/ransack-4.4.1/lib/ransack/locale/pt-BR.yml +70 -0
  43. data/ransack-4.4.1/lib/ransack/locale/ro.yml +70 -0
  44. data/ransack-4.4.1/lib/ransack/locale/ru.yml +70 -0
  45. data/ransack-4.4.1/lib/ransack/locale/sk.yml +70 -0
  46. data/ransack-4.4.1/lib/ransack/locale/sv.yml +70 -0
  47. data/ransack-4.4.1/lib/ransack/locale/tr.yml +70 -0
  48. data/ransack-4.4.1/lib/ransack/locale/uk.yml +72 -0
  49. data/ransack-4.4.1/lib/ransack/locale/zh-CN.yml +70 -0
  50. data/ransack-4.4.1/lib/ransack/locale/zh-TW.yml +70 -0
  51. data/ransack-4.4.1/lib/ransack/naming.rb +58 -0
  52. data/ransack-4.4.1/lib/ransack/nodes/attribute.rb +59 -0
  53. data/ransack-4.4.1/lib/ransack/nodes/bindable.rb +52 -0
  54. data/ransack-4.4.1/lib/ransack/nodes/condition.rb +399 -0
  55. data/ransack-4.4.1/lib/ransack/nodes/grouping.rb +206 -0
  56. data/ransack-4.4.1/lib/ransack/nodes/node.rb +34 -0
  57. data/ransack-4.4.1/lib/ransack/nodes/sort.rb +54 -0
  58. data/ransack-4.4.1/lib/ransack/nodes/value.rb +115 -0
  59. data/ransack-4.4.1/lib/ransack/predicate.rb +75 -0
  60. data/ransack-4.4.1/lib/ransack/ransacker.rb +24 -0
  61. data/ransack-4.4.1/lib/ransack/search.rb +195 -0
  62. data/ransack-4.4.1/lib/ransack/translate.rb +156 -0
  63. data/ransack-4.4.1/lib/ransack/version.rb +3 -0
  64. data/ransack-4.4.1/lib/ransack/visitor.rb +87 -0
  65. data/ransack-4.4.1/lib/ransack.rb +33 -0
  66. data/ransack-4.4.1/spec/console.rb +13 -0
  67. data/ransack-4.4.1/spec/factories/articles.rb +7 -0
  68. data/ransack-4.4.1/spec/factories/comments.rb +7 -0
  69. data/ransack-4.4.1/spec/factories/notes.rb +13 -0
  70. data/ransack-4.4.1/spec/factories/people.rb +10 -0
  71. data/ransack-4.4.1/spec/factories/tags.rb +5 -0
  72. data/ransack-4.4.1/spec/helpers/polyamorous_helper.rb +13 -0
  73. data/ransack-4.4.1/spec/helpers/ransack_helper.rb +9 -0
  74. data/ransack-4.4.1/spec/polyamorous/activerecord_compatibility_spec.rb +15 -0
  75. data/ransack-4.4.1/spec/polyamorous/join_association_spec.rb +29 -0
  76. data/ransack-4.4.1/spec/polyamorous/join_dependency_spec.rb +80 -0
  77. data/ransack-4.4.1/spec/polyamorous/join_spec.rb +19 -0
  78. data/ransack-4.4.1/spec/ransack/adapters/active_record/base_spec.rb +943 -0
  79. data/ransack-4.4.1/spec/ransack/adapters/active_record/context_spec.rb +219 -0
  80. data/ransack-4.4.1/spec/ransack/configuration_spec.rb +201 -0
  81. data/ransack-4.4.1/spec/ransack/helpers/form_builder_spec.rb +169 -0
  82. data/ransack-4.4.1/spec/ransack/helpers/form_helper_spec.rb +1075 -0
  83. data/ransack-4.4.1/spec/ransack/invalid_search_error_spec.rb +27 -0
  84. data/ransack-4.4.1/spec/ransack/nodes/condition_spec.rb +333 -0
  85. data/ransack-4.4.1/spec/ransack/nodes/grouping_spec.rb +111 -0
  86. data/ransack-4.4.1/spec/ransack/nodes/value_spec.rb +126 -0
  87. data/ransack-4.4.1/spec/ransack/predicate_spec.rb +504 -0
  88. data/ransack-4.4.1/spec/ransack/ransacker_spec.rb +69 -0
  89. data/ransack-4.4.1/spec/ransack/search_spec.rb +821 -0
  90. data/ransack-4.4.1/spec/ransack/translate_spec.rb +16 -0
  91. data/ransack-4.4.1/spec/spec_helper.rb +52 -0
  92. data/ransack-4.4.1/spec/support/en.yml +18 -0
  93. data/ransack-4.4.1/spec/support/schema.rb +419 -0
  94. metadata +133 -0
@@ -0,0 +1,821 @@
1
+ require 'spec_helper'
2
+
3
+ module Ransack
4
+ describe Search do
5
+ describe '#initialize' do
6
+ it 'removes empty conditions before building' do
7
+ expect_any_instance_of(Search).to receive(:build).with({})
8
+ Search.new(Person, name_eq: '')
9
+ end
10
+
11
+ it 'keeps conditions with a false value before building' do
12
+ expect_any_instance_of(Search).to receive(:build)
13
+ .with({ 'name_eq' => false })
14
+ Search.new(Person, name_eq: false)
15
+ end
16
+
17
+ it 'keeps conditions with a value before building' do
18
+ expect_any_instance_of(Search).to receive(:build)
19
+ .with({ 'name_eq' => 'foobar' })
20
+ Search.new(Person, name_eq: 'foobar')
21
+ end
22
+
23
+ context 'whitespace stripping' do
24
+ context 'when whitespace_strip option is true' do
25
+ before do
26
+ Ransack.configure { |c| c.strip_whitespace = true }
27
+ end
28
+
29
+ it 'strips leading & trailing whitespace before building' do
30
+ expect_any_instance_of(Search).to receive(:build)
31
+ .with({ 'name_eq' => 'foobar' })
32
+ Search.new(Person, name_eq: ' foobar ')
33
+ end
34
+ end
35
+
36
+ context 'when whitespace_strip option is false' do
37
+ before do
38
+ Ransack.configure { |c| c.strip_whitespace = false }
39
+ end
40
+
41
+ it 'doesn\'t strip leading & trailing whitespace before building' do
42
+ expect_any_instance_of(Search).to receive(:build)
43
+ .with({ 'name_eq' => ' foobar ' })
44
+ Search.new(Person, name_eq: ' foobar ')
45
+ end
46
+ end
47
+
48
+ it 'strips leading & trailing whitespace when strip_whitespace search parameter is true' do
49
+ expect_any_instance_of(Search).to receive(:build)
50
+ .with({ 'name_eq' => 'foobar' })
51
+ Search.new(Person, { name_eq: ' foobar ' }, { strip_whitespace: true })
52
+ end
53
+
54
+ it 'doesn\'t strip leading & trailing whitespace when strip_whitespace search parameter is false' do
55
+ expect_any_instance_of(Search).to receive(:build)
56
+ .with({ 'name_eq' => ' foobar ' })
57
+ Search.new(Person, { name_eq: ' foobar ' }, { strip_whitespace: false })
58
+ end
59
+ end
60
+
61
+ it 'removes empty suffixed conditions before building' do
62
+ expect_any_instance_of(Search).to receive(:build).with({})
63
+ Search.new(Person, name_eq_any: [''])
64
+ end
65
+
66
+ it 'keeps suffixed conditions with a false value before building' do
67
+ expect_any_instance_of(Search).to receive(:build)
68
+ .with({ 'name_eq_any' => [false] })
69
+ Search.new(Person, name_eq_any: [false])
70
+ end
71
+
72
+ it 'keeps suffixed conditions with a value before building' do
73
+ expect_any_instance_of(Search).to receive(:build)
74
+ .with({ 'name_eq_any' => ['foobar'] })
75
+ Search.new(Person, name_eq_any: ['foobar'])
76
+ end
77
+
78
+ it 'does not raise exception for string :params argument' do
79
+ expect { Search.new(Person, '') }.not_to raise_error
80
+ end
81
+
82
+ it 'accepts a context option' do
83
+ shared_context = Context.for(Person)
84
+ s1 = Search.new(Person, { name_eq: 'A' }, context: shared_context)
85
+ s2 = Search.new(Person, { name_eq: 'B' }, context: shared_context)
86
+ expect(s1.context).to be s2.context
87
+ end
88
+ end
89
+
90
+ describe '#build' do
91
+ it 'creates conditions for top-level attributes' do
92
+ s = Search.new(Person, name_eq: 'Ernie')
93
+ condition = s.base[:name_eq]
94
+ expect(condition).to be_a Nodes::Condition
95
+ expect(condition.predicate.name).to eq 'eq'
96
+ expect(condition.attributes.first.name).to eq 'name'
97
+ expect(condition.value).to eq 'Ernie'
98
+ end
99
+
100
+ it 'creates conditions for association attributes' do
101
+ s = Search.new(Person, children_name_eq: 'Ernie')
102
+ condition = s.base[:children_name_eq]
103
+ expect(condition).to be_a Nodes::Condition
104
+ expect(condition.predicate.name).to eq 'eq'
105
+ expect(condition.attributes.first.name).to eq 'children_name'
106
+ expect(condition.value).to eq 'Ernie'
107
+ end
108
+
109
+ it 'creates conditions for polymorphic belongs_to association attributes' do
110
+ s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
111
+ condition = s.base[:notable_of_Person_type_name_eq]
112
+ expect(condition).to be_a Nodes::Condition
113
+ expect(condition.predicate.name).to eq 'eq'
114
+ expect(condition.attributes.first.name)
115
+ .to eq 'notable_of_Person_type_name'
116
+ expect(condition.value).to eq 'Ernie'
117
+ end
118
+
119
+ it 'creates conditions for multiple polymorphic belongs_to association
120
+ attributes' do
121
+ s = Search.new(Note,
122
+ notable_of_Person_type_name_or_notable_of_Article_type_title_eq: 'Ernie')
123
+ condition = s.
124
+ base[:notable_of_Person_type_name_or_notable_of_Article_type_title_eq]
125
+ expect(condition).to be_a Nodes::Condition
126
+ expect(condition.predicate.name).to eq 'eq'
127
+ expect(condition.attributes.first.name)
128
+ .to eq 'notable_of_Person_type_name'
129
+ expect(condition.attributes.last.name)
130
+ .to eq 'notable_of_Article_type_title'
131
+ expect(condition.value).to eq 'Ernie'
132
+ end
133
+
134
+ it 'creates conditions for aliased attributes',
135
+ if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
136
+ s = Search.new(Person, full_name_eq: 'Ernie')
137
+ condition = s.base[:full_name_eq]
138
+ expect(condition).to be_a Nodes::Condition
139
+ expect(condition.predicate.name).to eq 'eq'
140
+ expect(condition.attributes.first.name).to eq 'full_name'
141
+ expect(condition.value).to eq 'Ernie'
142
+ end
143
+
144
+ it 'preserves default scope and conditions for associations' do
145
+ s = Search.new(Person, published_articles_title_eq: 'Test')
146
+ expect(s.result.to_sql).to include 'default_scope'
147
+ expect(s.result.to_sql).to include 'published'
148
+ end
149
+
150
+ # The failure/oversight in Ransack::Nodes::Condition#arel_predicate or deeper is beyond my understanding of the structures
151
+ it 'preserves (inverts) default scope and conditions for negative subqueries' do
152
+ # the positive case (published_articles_title_eq) is
153
+ # SELECT "people".* FROM "people"
154
+ # LEFT OUTER JOIN "articles" ON "articles"."person_id" = "people"."id"
155
+ # AND "articles"."published" = 't'
156
+ # AND ('default_scope' = 'default_scope')
157
+ # WHERE "articles"."title" = 'Test' ORDER BY "people"."id" DESC
158
+ #
159
+ # negative case was
160
+ # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
161
+ # SELECT "articles"."person_id" FROM "articles"
162
+ # WHERE "articles"."person_id" = "people"."id"
163
+ # AND NOT ("articles"."title" != 'Test')
164
+ # ) ORDER BY "people"."id" DESC
165
+ #
166
+ # Should have been like
167
+ # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
168
+ # SELECT "articles"."person_id" FROM "articles"
169
+ # WHERE "articles"."person_id" = "people"."id"
170
+ # AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
171
+ # ) ORDER BY "people"."id" DESC
172
+ #
173
+ # With tenanting (eg default_scope with column reference), NOT IN should be like
174
+ # SELECT "people".* FROM "people" WHERE "people"."tenant_id" = 'tenant_id' AND "people"."id" NOT IN (
175
+ # SELECT "articles"."person_id" FROM "articles"
176
+ # WHERE "articles"."person_id" = "people"."id"
177
+ # AND "articles"."tenant_id" = 'tenant_id'
178
+ # AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
179
+ # ) ORDER BY "people"."id" DESC
180
+
181
+ s = Search.new(Person, published_articles_title_not_eq: 'Test')
182
+ expect(s.result.to_sql).to include 'default_scope'
183
+ expect(s.result.to_sql).to include 'published'
184
+ end
185
+
186
+ it 'discards empty conditions' do
187
+ s = Search.new(Person, children_name_eq: '')
188
+ condition = s.base[:children_name_eq]
189
+ expect(condition).to be_nil
190
+ end
191
+
192
+ it 'accepts base grouping condition as an option' do
193
+ expect(Nodes::Grouping).to receive(:new).with(kind_of(Context), 'or')
194
+ Search.new(Person, {}, { grouping: 'or' })
195
+ end
196
+
197
+ it 'accepts arrays of groupings' do
198
+ s = Search.new(Person,
199
+ g: [
200
+ { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
201
+ { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
202
+ ]
203
+ )
204
+ ors = s.groupings
205
+ expect(ors.size).to eq(2)
206
+ or1, or2 = ors
207
+ expect(or1).to be_a Nodes::Grouping
208
+ expect(or1.combinator).to eq 'or'
209
+ expect(or2).to be_a Nodes::Grouping
210
+ expect(or2.combinator).to eq 'or'
211
+ end
212
+
213
+ it 'accepts attributes hashes for groupings' do
214
+ s = Search.new(Person,
215
+ g: {
216
+ '0' => { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
217
+ '1' => { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
218
+ }
219
+ )
220
+ ors = s.groupings
221
+ expect(ors.size).to eq(2)
222
+ or1, or2 = ors
223
+ expect(or1).to be_a Nodes::Grouping
224
+ expect(or1.combinator).to eq 'or'
225
+ expect(or2).to be_a Nodes::Grouping
226
+ expect(or2.combinator).to eq 'or'
227
+ end
228
+
229
+ it 'accepts attributes hashes for conditions' do
230
+ s = Search.new(Person,
231
+ c: {
232
+ '0' => { a: ['name'], p: 'eq', v: ['Ernie'] },
233
+ '1' => {
234
+ a: ['children_name', 'parent_name'],
235
+ p: 'eq', v: ['Ernie'], m: 'or'
236
+ }
237
+ }
238
+ )
239
+ conditions = s.base.conditions
240
+ expect(conditions.size).to eq(2)
241
+ expect(conditions.map { |c| c.class })
242
+ .to eq [Nodes::Condition, Nodes::Condition]
243
+ end
244
+
245
+ it 'creates conditions for custom predicates that take arrays' do
246
+ Ransack.configure do |config|
247
+ config.add_predicate 'ary_pred', wants_array: true
248
+ end
249
+
250
+ s = Search.new(Person, name_ary_pred: ['Ernie', 'Bert'])
251
+ condition = s.base[:name_ary_pred]
252
+ expect(condition).to be_a Nodes::Condition
253
+ expect(condition.predicate.name).to eq 'ary_pred'
254
+ expect(condition.attributes.first.name).to eq 'name'
255
+ expect(condition.value).to eq ['Ernie', 'Bert']
256
+ end
257
+
258
+ it 'does not evaluate the query on #inspect' do
259
+ s = Search.new(Person, children_id_in: [1, 2, 3])
260
+ expect(s.inspect).not_to match /ActiveRecord/
261
+ end
262
+
263
+ context 'with an invalid condition' do
264
+ subject { Search.new(Person, unknown_attr_eq: 'Ernie') }
265
+
266
+ context 'when ignore_unknown_conditions configuration option is false' do
267
+ before do
268
+ Ransack.configure { |c| c.ignore_unknown_conditions = false }
269
+ end
270
+
271
+ specify { expect { subject }.to raise_error ArgumentError }
272
+ specify { expect { subject }.to raise_error InvalidSearchError }
273
+ end
274
+
275
+ context 'when ignore_unknown_conditions configuration option is true' do
276
+ before do
277
+ Ransack.configure { |c| c.ignore_unknown_conditions = true }
278
+ end
279
+
280
+ specify { expect { subject }.not_to raise_error }
281
+ end
282
+
283
+ subject(:with_ignore_unknown_conditions_false) {
284
+ Search.new(Person,
285
+ { unknown_attr_eq: 'Ernie' },
286
+ { ignore_unknown_conditions: false }
287
+ )
288
+ }
289
+
290
+ subject(:with_ignore_unknown_conditions_true) {
291
+ Search.new(Person,
292
+ { unknown_attr_eq: 'Ernie' },
293
+ { ignore_unknown_conditions: true }
294
+ )
295
+ }
296
+
297
+ context 'when ignore_unknown_conditions search parameter is absent' do
298
+ specify { expect { subject }.not_to raise_error }
299
+ end
300
+
301
+ context 'when ignore_unknown_conditions search parameter is false' do
302
+ specify { expect { with_ignore_unknown_conditions_false }.to raise_error ArgumentError }
303
+ specify { expect { with_ignore_unknown_conditions_false }.to raise_error InvalidSearchError }
304
+ end
305
+
306
+ context 'when ignore_unknown_conditions search parameter is true' do
307
+ specify { expect { with_ignore_unknown_conditions_true }.not_to raise_error }
308
+ end
309
+ end
310
+
311
+ it 'does not modify the parameters' do
312
+ params = { name_eq: '' }
313
+ expect { Search.new(Person, params) }.not_to change { params }
314
+ end
315
+
316
+ context 'with empty search parameters' do
317
+ it 'handles completely empty parameters' do
318
+ search = Search.new(Person, {})
319
+ expect(search.result.to_sql).not_to match(/WHERE/)
320
+ end
321
+
322
+ it 'handles nil parameters' do
323
+ search = Search.new(Person, nil)
324
+ expect(search.result.to_sql).not_to match(/WHERE/)
325
+ end
326
+ end
327
+
328
+ context 'with whitespace-only values' do
329
+ before do
330
+ Ransack.configure { |c| c.strip_whitespace = true }
331
+ end
332
+
333
+ it 'removes whitespace-only values' do
334
+ expect_any_instance_of(Search).to receive(:build).with({})
335
+ Search.new(Person, name_eq: ' ')
336
+ end
337
+
338
+ it 'keeps values with content after whitespace stripping' do
339
+ expect_any_instance_of(Search).to receive(:build).with({ 'name_eq' => 'test' })
340
+ Search.new(Person, name_eq: ' test ')
341
+ end
342
+ end
343
+
344
+ context 'with special characters in values' do
345
+ it 'handles values with special regex characters' do
346
+ search = Search.new(Person, name_cont: 'test[(){}^$|?*+.\\')
347
+ expect { search.result }.not_to raise_error
348
+ end
349
+
350
+ it 'handles values with SQL injection attempts' do
351
+ search = Search.new(Person, name_cont: "'; DROP TABLE people; --")
352
+ expect { search.result }.not_to raise_error
353
+ end
354
+ end
355
+
356
+ context "ransackable_scope" do
357
+ around(:each) do |example|
358
+ Person.define_singleton_method(:name_eq) do |name|
359
+ self.where(name: name)
360
+ end
361
+
362
+ begin
363
+ example.run
364
+ ensure
365
+ Person.singleton_class.undef_method :name_eq
366
+ end
367
+ end
368
+
369
+ it "is prioritized over base predicates" do
370
+ allow(Person).to receive(:ransackable_scopes)
371
+ .and_return(Person.ransackable_scopes + [:name_eq])
372
+
373
+ s = Search.new(Person, name_eq: "Johny")
374
+ expect(s.instance_variable_get(:@scope_args)["name_eq"]).to eq("Johny")
375
+ expect(s.base[:name_eq]).to be_nil
376
+ end
377
+ end
378
+
379
+ context "ransackable_scope with array arguments" do
380
+ around(:each) do |example|
381
+ Person.define_singleton_method(:domestic) do |countries|
382
+ self.where(name: countries)
383
+ end
384
+
385
+ Person.define_singleton_method(:flexible_scope) do |*args|
386
+ self.where(id: args)
387
+ end
388
+
389
+ Person.define_singleton_method(:two_param_scope) do |param1, param2|
390
+ self.where(name: param1, id: param2)
391
+ end
392
+
393
+ begin
394
+ example.run
395
+ ensure
396
+ Person.singleton_class.undef_method :domestic
397
+ Person.singleton_class.undef_method :flexible_scope
398
+ Person.singleton_class.undef_method :two_param_scope
399
+ end
400
+ end
401
+
402
+ it "handles scopes that take arrays as single arguments (arity 1)" do
403
+ allow(Person).to receive(:ransackable_scopes)
404
+ .and_return(Person.ransackable_scopes + [:domestic])
405
+
406
+ # This should not raise ArgumentError
407
+ expect {
408
+ s = Search.new(Person, domestic: ['US', 'JP'])
409
+ s.result # This triggers the actual scope call
410
+ }.not_to raise_error
411
+
412
+ s = Search.new(Person, domestic: ['US', 'JP'])
413
+ expect(s.instance_variable_get(:@scope_args)["domestic"]).to eq("US")
414
+ end
415
+
416
+ it "handles scopes with flexible arity (negative arity)" do
417
+ allow(Person).to receive(:ransackable_scopes)
418
+ .and_return(Person.ransackable_scopes + [:flexible_scope])
419
+
420
+ expect {
421
+ s = Search.new(Person, flexible_scope: ['US', 'JP'])
422
+ s.result
423
+ }.not_to raise_error
424
+ end
425
+
426
+ it "handles scopes with arity > 1" do
427
+ allow(Person).to receive(:ransackable_scopes)
428
+ .and_return(Person.ransackable_scopes + [:two_param_scope])
429
+
430
+ expect {
431
+ s = Search.new(Person, two_param_scope: ['param1', 'param2'])
432
+ s.result
433
+ }.not_to raise_error
434
+ end
435
+
436
+ it "still supports the workaround with nested arrays" do
437
+ allow(Person).to receive(:ransackable_scopes)
438
+ .and_return(Person.ransackable_scopes + [:domestic])
439
+
440
+ # The workaround from the issue should still work
441
+ expect {
442
+ s = Search.new(Person, domestic: [['US', 'JP']])
443
+ s.result
444
+ }.not_to raise_error
445
+ end
446
+ end
447
+ end
448
+
449
+ describe '#result' do
450
+ let(:people_name_field) {
451
+ "#{quote_table_name("people")}.#{quote_column_name("name")}"
452
+ }
453
+ let(:children_people_name_field) {
454
+ "#{quote_table_name("children_people")}.#{quote_column_name("name")}"
455
+ }
456
+ let(:notable_type_field) {
457
+ "#{quote_table_name("notes")}.#{quote_column_name("notable_type")}"
458
+ }
459
+
460
+ it 'evaluates conditions contextually' do
461
+ s = Search.new(Person, children_name_eq: 'Ernie')
462
+ expect(s.result).to be_an ActiveRecord::Relation
463
+ expect(s.result.to_sql).to match /#{
464
+ children_people_name_field} = 'Ernie'/
465
+ end
466
+
467
+ it 'use appropriate table alias' do
468
+ s = Search.new(Person, {
469
+ name_eq: "person_name_query",
470
+ articles_title_eq: "person_article_title_query",
471
+ parent_name_eq: "parent_name_query",
472
+ parent_articles_title_eq: 'parents_article_title_query'
473
+ }).result
474
+
475
+ real_query = remove_quotes_and_backticks(s.to_sql)
476
+
477
+ expect(real_query)
478
+ .to match(%r{LEFT OUTER JOIN articles ON (\('default_scope' = 'default_scope'\) AND )?articles.person_id = people.id})
479
+ expect(real_query)
480
+ .to match(%r{LEFT OUTER JOIN articles articles_people ON (\('default_scope' = 'default_scope'\) AND )?articles_people.person_id = parents_people.id})
481
+
482
+ expect(real_query)
483
+ .to include "people.name = 'person_name_query'"
484
+ expect(real_query)
485
+ .to include "articles.title = 'person_article_title_query'"
486
+ expect(real_query)
487
+ .to include "parents_people.name = 'parent_name_query'"
488
+ expect(real_query)
489
+ .to include "articles_people.title = 'parents_article_title_query'"
490
+ end
491
+
492
+ it 'evaluates conditions for multiple `belongs_to` associations to the same table contextually' do
493
+ s = Search.new(
494
+ Recommendation,
495
+ person_name_eq: 'Ernie',
496
+ target_person_parent_name_eq: 'Test'
497
+ ).result
498
+ expect(s).to be_an ActiveRecord::Relation
499
+ real_query = remove_quotes_and_backticks(s.to_sql)
500
+ expected_query = <<-SQL
501
+ SELECT recommendations.* FROM recommendations
502
+ LEFT OUTER JOIN people ON people.id = recommendations.person_id
503
+ LEFT OUTER JOIN people target_people_recommendations
504
+ ON target_people_recommendations.id = recommendations.target_person_id
505
+ LEFT OUTER JOIN people parents_people
506
+ ON parents_people.id = target_people_recommendations.parent_id
507
+ WHERE (people.name = 'Ernie' AND parents_people.name = 'Test')
508
+ SQL
509
+ .squish
510
+ expect(real_query).to eq expected_query
511
+ end
512
+
513
+ it 'evaluates compound conditions contextually' do
514
+ s = Search.new(Person, children_name_or_name_eq: 'Ernie').result
515
+ expect(s).to be_an ActiveRecord::Relation
516
+ expect(s.to_sql).to match /#{children_people_name_field
517
+ } = 'Ernie' OR #{people_name_field} = 'Ernie'/
518
+ end
519
+
520
+ it 'evaluates polymorphic belongs_to association conditions contextually' do
521
+ s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie').result
522
+ expect(s).to be_an ActiveRecord::Relation
523
+ expect(s.to_sql).to match /#{people_name_field} = 'Ernie'/
524
+ expect(s.to_sql).to match /#{notable_type_field} = 'Person'/
525
+ end
526
+
527
+ it 'evaluates nested conditions' do
528
+ s = Search.new(Person, children_name_eq: 'Ernie',
529
+ g: [
530
+ { m: 'or', name_eq: 'Ernie', children_children_name_eq: 'Ernie' }
531
+ ]
532
+ ).result
533
+ expect(s).to be_an ActiveRecord::Relation
534
+ first, last = s.to_sql.split(/ AND /)
535
+ expect(first).to match /#{children_people_name_field} = 'Ernie'/
536
+ expect(last).to match /#{
537
+ people_name_field} = 'Ernie' OR #{
538
+ quote_table_name("children_people_2")}.#{
539
+ quote_column_name("name")} = 'Ernie'/
540
+ end
541
+
542
+ it 'evaluates arrays of groupings' do
543
+ s = Search.new(Person,
544
+ g: [
545
+ { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
546
+ { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' }
547
+ ]
548
+ ).result
549
+ expect(s).to be_an ActiveRecord::Relation
550
+ first, last = s.to_sql.split(/ AND /)
551
+ expect(first).to match /#{people_name_field} = 'Ernie' OR #{
552
+ children_people_name_field} = 'Ernie'/
553
+ expect(last).to match /#{people_name_field} = 'Bert' OR #{
554
+ children_people_name_field} = 'Bert'/
555
+ end
556
+
557
+ it 'returns distinct records when passed distinct: true' do
558
+ s = Search.new(Person,
559
+ g: [
560
+ { m: 'or', comments_body_cont: 'e', articles_comments_body_cont: 'e' }
561
+ ]
562
+ )
563
+
564
+ all_or_load, uniq_or_distinct = :load, :distinct
565
+ expect(s.result.send(all_or_load).size)
566
+ .to eq(8998)
567
+ expect(s.result(distinct: true).size)
568
+ .to eq(10)
569
+ expect(s.result.send(all_or_load).send(uniq_or_distinct))
570
+ .to eq s.result(distinct: true).send(all_or_load)
571
+ end
572
+
573
+ it 'evaluates joins with belongs_to join' do
574
+ s = Person.joins(:parent).ransack(parent_name_eq: 'Ernie').result(distinct: true)
575
+ expect(s).to be_an ActiveRecord::Relation
576
+ end
577
+
578
+ private
579
+
580
+ def remove_quotes_and_backticks(str)
581
+ str.gsub(/["`]/, '')
582
+ end
583
+ end
584
+
585
+ describe '#sorts=' do
586
+ before do
587
+ @s = Search.new(Person)
588
+ end
589
+
590
+ it 'doesn\'t creates sorts' do
591
+ @s.sorts = ''
592
+ expect(@s.sorts.size).to eq(0)
593
+ end
594
+
595
+ it 'creates sorts based on a single attribute/direction' do
596
+ @s.sorts = 'id desc'
597
+ expect(@s.sorts.size).to eq(1)
598
+ sort = @s.sorts.first
599
+ expect(sort).to be_a Nodes::Sort
600
+ expect(sort.name).to eq 'id'
601
+ expect(sort.dir).to eq 'desc'
602
+ end
603
+
604
+ it 'creates sorts based on a single attribute and uppercase direction' do
605
+ @s.sorts = 'id DESC'
606
+ expect(@s.sorts.size).to eq(1)
607
+ sort = @s.sorts.first
608
+ expect(sort).to be_a Nodes::Sort
609
+ expect(sort.name).to eq 'id'
610
+ expect(sort.dir).to eq 'desc'
611
+ end
612
+
613
+ it 'creates sorts based on a single attribute and without direction' do
614
+ @s.sorts = 'id'
615
+ expect(@s.sorts.size).to eq(1)
616
+ sort = @s.sorts.first
617
+ expect(sort).to be_a Nodes::Sort
618
+ expect(sort.name).to eq 'id'
619
+ expect(sort.dir).to eq 'asc'
620
+ end
621
+
622
+ it 'creates sorts based on a single alias/direction' do
623
+ @s.sorts = 'daddy desc'
624
+ expect(@s.sorts.size).to eq(1)
625
+ sort = @s.sorts.first
626
+ expect(sort).to be_a Nodes::Sort
627
+ expect(sort.name).to eq 'parent_name'
628
+ expect(sort.dir).to eq 'desc'
629
+ end
630
+
631
+ it 'creates sorts based on a single alias and uppercase direction' do
632
+ @s.sorts = 'daddy DESC'
633
+ expect(@s.sorts.size).to eq(1)
634
+ sort = @s.sorts.first
635
+ expect(sort).to be_a Nodes::Sort
636
+ expect(sort.name).to eq 'parent_name'
637
+ expect(sort.dir).to eq 'desc'
638
+ end
639
+
640
+ it 'creates sorts based on a single alias and without direction' do
641
+ @s.sorts = 'daddy'
642
+ expect(@s.sorts.size).to eq(1)
643
+ sort = @s.sorts.first
644
+ expect(sort).to be_a Nodes::Sort
645
+ expect(sort.name).to eq 'parent_name'
646
+ expect(sort.dir).to eq 'asc'
647
+ end
648
+
649
+ it 'creates sorts based on attributes, alias and directions in array format' do
650
+ @s.sorts = ['id desc', { name: 'daddy', dir: 'asc' }]
651
+ expect(@s.sorts.size).to eq(2)
652
+ sort1, sort2 = @s.sorts
653
+ expect(sort1).to be_a Nodes::Sort
654
+ expect(sort1.name).to eq 'id'
655
+ expect(sort1.dir).to eq 'desc'
656
+ expect(sort2).to be_a Nodes::Sort
657
+ expect(sort2.name).to eq 'parent_name'
658
+ expect(sort2.dir).to eq 'asc'
659
+ end
660
+
661
+ it 'creates sorts based on attributes, alias and uppercase directions in array format' do
662
+ @s.sorts = ['id DESC', { name: 'daddy', dir: 'ASC' }]
663
+ expect(@s.sorts.size).to eq(2)
664
+ sort1, sort2 = @s.sorts
665
+ expect(sort1).to be_a Nodes::Sort
666
+ expect(sort1.name).to eq 'id'
667
+ expect(sort1.dir).to eq 'desc'
668
+ expect(sort2).to be_a Nodes::Sort
669
+ expect(sort2.name).to eq 'parent_name'
670
+ expect(sort2.dir).to eq 'asc'
671
+ end
672
+
673
+ it 'creates sorts based on attributes, alias and different directions
674
+ in array format' do
675
+ @s.sorts = ['id DESC', { name: 'daddy', dir: nil }]
676
+ expect(@s.sorts.size).to eq(2)
677
+ sort1, sort2 = @s.sorts
678
+ expect(sort1).to be_a Nodes::Sort
679
+ expect(sort1.name).to eq 'id'
680
+ expect(sort1.dir).to eq 'desc'
681
+ expect(sort2).to be_a Nodes::Sort
682
+ expect(sort2.name).to eq 'parent_name'
683
+ expect(sort2.dir).to eq 'asc'
684
+ end
685
+
686
+ it 'creates sorts based on attributes, alias and directions in hash format' do
687
+ @s.sorts = {
688
+ '0' => { name: 'id', dir: 'desc' },
689
+ '1' => { name: 'daddy', dir: 'asc' }
690
+ }
691
+ expect(@s.sorts.size).to eq(2)
692
+ expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
693
+ id_sort = @s.sorts.detect { |s| s.name == 'id' }
694
+ daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
695
+ expect(id_sort.dir).to eq 'desc'
696
+ expect(daddy_sort.dir).to eq 'asc'
697
+ end
698
+
699
+ it 'creates sorts based on attributes, alias and uppercase directions
700
+ in hash format' do
701
+ @s.sorts = {
702
+ '0' => { name: 'id', dir: 'DESC' },
703
+ '1' => { name: 'daddy', dir: 'ASC' }
704
+ }
705
+ expect(@s.sorts.size).to eq(2)
706
+ expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
707
+ id_sort = @s.sorts.detect { |s| s.name == 'id' }
708
+ daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
709
+ expect(id_sort.dir).to eq 'desc'
710
+ expect(daddy_sort.dir).to eq 'asc'
711
+ end
712
+
713
+ it 'creates sorts based on attributes, alias and different directions
714
+ in hash format' do
715
+ @s.sorts = {
716
+ '0' => { name: 'id', dir: 'DESC' },
717
+ '1' => { name: 'daddy', dir: nil }
718
+ }
719
+ expect(@s.sorts.size).to eq(2)
720
+ expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
721
+ id_sort = @s.sorts.detect { |s| s.name == 'id' }
722
+ daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
723
+ expect(id_sort.dir).to eq 'desc'
724
+ expect(daddy_sort.dir).to eq 'asc'
725
+ end
726
+
727
+ it 'overrides existing sort' do
728
+ @s.sorts = 'id asc'
729
+ expect(@s.result.first.id).to eq 1
730
+ end
731
+
732
+ it 'raises ArgumentError when an invalid argument is sent' do
733
+ expect do
734
+ @s.sorts = 1234
735
+ end.to raise_error(ArgumentError, "Invalid argument (Integer) supplied to sorts=")
736
+ end
737
+
738
+ it 'raises InvalidSearchError when an invalid argument is sent' do
739
+ expect do
740
+ @s.sorts = 1234
741
+ end.to raise_error(Ransack::InvalidSearchError, "Invalid argument (Integer) supplied to sorts=")
742
+ end
743
+
744
+ it "PG's sort option", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
745
+ default = Ransack.options.clone
746
+
747
+ s = Search.new(Person, s: 'name asc')
748
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC"
749
+
750
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_first }
751
+ s = Search.new(Person, s: 'name asc')
752
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC NULLS FIRST"
753
+ s = Search.new(Person, s: 'name desc')
754
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" DESC NULLS LAST"
755
+
756
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_last }
757
+ s = Search.new(Person, s: 'name asc')
758
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC NULLS LAST"
759
+ s = Search.new(Person, s: 'name desc')
760
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" DESC NULLS FIRST"
761
+
762
+ Ransack.options = default
763
+ end
764
+
765
+ it "PG's sort option with double name", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
766
+ default = Ransack.options.clone
767
+
768
+ s = Search.new(Person, s: 'doubled_name asc')
769
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC"
770
+
771
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_first }
772
+ s = Search.new(Person, s: 'doubled_name asc')
773
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS FIRST"
774
+ s = Search.new(Person, s: 'doubled_name desc')
775
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS LAST"
776
+
777
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_last }
778
+ s = Search.new(Person, s: 'doubled_name asc')
779
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS LAST"
780
+ s = Search.new(Person, s: 'doubled_name desc')
781
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS FIRST"
782
+
783
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_always_first }
784
+ s = Search.new(Person, s: 'doubled_name asc')
785
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS FIRST"
786
+ s = Search.new(Person, s: 'doubled_name desc')
787
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS FIRST"
788
+
789
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_always_last }
790
+ s = Search.new(Person, s: 'doubled_name asc')
791
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS LAST"
792
+ s = Search.new(Person, s: 'doubled_name desc')
793
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS LAST"
794
+
795
+ Ransack.options = default
796
+ end
797
+ end
798
+
799
+ describe '#method_missing' do
800
+ before do
801
+ @s = Search.new(Person)
802
+ end
803
+
804
+ it 'raises NoMethodError when sent an invalid attribute' do
805
+ expect { @s.blah }.to raise_error NoMethodError
806
+ end
807
+
808
+ it 'sets condition attributes when sent valid attributes' do
809
+ @s.name_eq = 'Ernie'
810
+ expect(@s.name_eq).to eq 'Ernie'
811
+ end
812
+
813
+ it 'allows chaining to access nested conditions' do
814
+ @s.groupings = [
815
+ { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' }
816
+ ]
817
+ expect(@s.groupings.first.children_name_eq).to eq 'Ernie'
818
+ end
819
+ end
820
+ end
821
+ end