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,943 @@
1
+ require 'spec_helper'
2
+
3
+ module Ransack
4
+ module Adapters
5
+ module ActiveRecord
6
+ describe Base do
7
+ subject { ::ActiveRecord::Base }
8
+
9
+ it { should respond_to :ransack }
10
+
11
+ describe '#search' do
12
+ subject { Person.ransack }
13
+
14
+ it { should be_a Search }
15
+ it 'has a Relation as its object' do
16
+ expect(subject.object).to be_an ::ActiveRecord::Relation
17
+ end
18
+
19
+ context "multiple database connection" do
20
+ it "does not raise error" do
21
+ expect { Person.ransack(name_cont: "test") }.not_to raise_error
22
+ expect { SubDB::OperationHistory.ransack(people_id_eq: 1) }.not_to raise_error
23
+ end
24
+ end
25
+
26
+ context 'with scopes' do
27
+ before do
28
+ allow(Person)
29
+ .to receive(:ransackable_scopes)
30
+ .and_return([:active, :over_age, :of_age])
31
+ end
32
+
33
+ it 'applies true scopes' do
34
+ s = Person.ransack('active' => true)
35
+ expect(s.result.to_sql).to (include 'active = 1')
36
+ end
37
+
38
+ it 'applies stringy true scopes' do
39
+ s = Person.ransack('active' => 'true')
40
+ expect(s.result.to_sql).to (include 'active = 1')
41
+ end
42
+
43
+ it 'applies stringy boolean scopes with true value in an array' do
44
+ s = Person.ransack('of_age' => ['true'])
45
+ expect(s.result.to_sql).to (include rails7_and_mysql ? %q{(age >= '18')} : 'age >= 18')
46
+ end
47
+
48
+ it 'applies stringy boolean scopes with false value in an array' do
49
+ s = Person.ransack('of_age' => ['false'])
50
+ expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age < '18'} : 'age < 18')
51
+ end
52
+
53
+ it 'ignores unlisted scopes' do
54
+ s = Person.ransack('restricted' => true)
55
+ expect(s.result.to_sql).to_not (include 'restricted')
56
+ end
57
+
58
+ it 'ignores false scopes' do
59
+ s = Person.ransack('active' => false)
60
+ expect(s.result.to_sql).not_to (include 'active')
61
+ end
62
+
63
+ it 'ignores stringy false scopes' do
64
+ s = Person.ransack('active' => 'false')
65
+ expect(s.result.to_sql).to_not (include 'active')
66
+ end
67
+
68
+ it 'passes values to scopes' do
69
+ s = Person.ransack('over_age' => 18)
70
+ expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '18'} : 'age > 18')
71
+ end
72
+
73
+ it 'chains scopes' do
74
+ s = Person.ransack('over_age' => 18, 'active' => true)
75
+ expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '18'} : 'age > 18')
76
+ expect(s.result.to_sql).to (include 'active = 1')
77
+ end
78
+
79
+ it 'applies scopes that define string SQL joins' do
80
+ allow(Article)
81
+ .to receive(:ransackable_scopes)
82
+ .and_return([:latest_comment_cont])
83
+
84
+ # Including a negative condition to test removing the scope
85
+ s = Search.new(Article, notes_note_not_eq: 'Test', latest_comment_cont: 'Test')
86
+ expect(s.result.to_sql).to include 'latest_comment'
87
+ end
88
+
89
+ context "with sanitize_custom_scope_booleans set to false" do
90
+ before(:all) do
91
+ Ransack.configure { |c| c.sanitize_custom_scope_booleans = false }
92
+ end
93
+
94
+ after(:all) do
95
+ Ransack.configure { |c| c.sanitize_custom_scope_booleans = true }
96
+ end
97
+
98
+ it 'passes true values to scopes' do
99
+ s = Person.ransack('over_age' => 1)
100
+ expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '1'} : 'age > 1')
101
+ end
102
+
103
+ it 'passes false values to scopes' do
104
+ s = Person.ransack('over_age' => 0)
105
+ expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '0'} : 'age > 0')
106
+ end
107
+ end
108
+
109
+ context "with ransackable_scopes_skip_sanitize_args enabled for scope" do
110
+ before do
111
+ allow(Person)
112
+ .to receive(:ransackable_scopes_skip_sanitize_args)
113
+ .and_return([:over_age])
114
+ end
115
+
116
+ it 'passes true values to scopes' do
117
+ s = Person.ransack('over_age' => 1)
118
+ expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '1'} : 'age > 1')
119
+ end
120
+
121
+ it 'passes false values to scopes' do
122
+ s = Person.ransack('over_age' => 0)
123
+ expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '0'} : 'age > 0')
124
+ end
125
+ end
126
+ end
127
+
128
+ it 'does not raise exception for string :params argument' do
129
+ expect { Person.ransack('') }.to_not raise_error
130
+ end
131
+
132
+ it 'raises ArgumentError exception if ransack! called with unknown condition' do
133
+ expect { Person.ransack!(unknown_attr_eq: 'Ernie') }.to raise_error(ArgumentError)
134
+ end
135
+
136
+ it 'raises InvalidSearchError exception if ransack! called with unknown condition' do
137
+ expect { Person.ransack!(unknown_attr_eq: 'Ernie') }.to raise_error(InvalidSearchError)
138
+ end
139
+
140
+ it 'does not modify the parameters' do
141
+ params = { name_eq: '' }
142
+ expect { Person.ransack(params) }.not_to change { params }
143
+ end
144
+ end
145
+
146
+ context 'has_one through associations' do
147
+ let(:address) { Address.create!(city: 'Boston') }
148
+ let(:org) { Organization.create!(name: 'Testorg', address: address) }
149
+ let!(:employee) { Employee.create!(name: 'Ernie', organization: org) }
150
+
151
+ it 'works when has_one through association is first' do
152
+ s = Employee.ransack(address_city_eq: 'Boston', organization_name_eq: 'Testorg')
153
+ expect(s.result.to_a).to include(employee)
154
+ end
155
+
156
+ it 'works when has_one through association is last' do
157
+ s = Employee.ransack(organization_name_eq: 'Testorg', address_city_eq: 'Boston')
158
+ expect(s.result.to_a).to include(employee)
159
+ end
160
+ end
161
+
162
+ context 'negative conditions on HABTM associations' do
163
+ let(:medieval) { Tag.create!(name: 'Medieval') }
164
+ let(:fantasy) { Tag.create!(name: 'Fantasy') }
165
+ let(:arthur) { Article.create!(title: 'King Arthur') }
166
+ let(:marco) { Article.create!(title: 'Marco Polo') }
167
+
168
+ before do
169
+ marco.tags << medieval
170
+ arthur.tags << medieval
171
+ arthur.tags << fantasy
172
+ end
173
+
174
+ it 'removes redundant joins from top query' do
175
+ s = Article.ransack(tags_name_not_eq: "Fantasy")
176
+ sql = s.result.to_sql
177
+ expect(sql).to_not include('LEFT OUTER JOIN')
178
+ end
179
+
180
+ it 'handles != for single values' do
181
+ s = Article.ransack(tags_name_not_eq: "Fantasy")
182
+ articles = s.result.to_a
183
+ expect(articles).to include marco
184
+ expect(articles).to_not include arthur
185
+ end
186
+
187
+ it 'handles NOT IN for multiple attributes' do
188
+ s = Article.ransack(tags_name_not_in: ["Fantasy", "Scifi"])
189
+ articles = s.result.to_a
190
+
191
+ expect(articles).to include marco
192
+ expect(articles).to_not include arthur
193
+ end
194
+ end
195
+
196
+ context 'negative conditions on related object with HABTM associations' do
197
+ let(:medieval) { Tag.create!(name: 'Medieval') }
198
+ let(:fantasy) { Tag.create!(name: 'Fantasy') }
199
+ let(:arthur) { Article.create!(title: 'King Arthur') }
200
+ let(:marco) { Article.create!(title: 'Marco Polo') }
201
+ let(:comment_arthur) { marco.comments.create!(body: 'King Arthur comment') }
202
+ let(:comment_marco) { arthur.comments.create!(body: 'Marco Polo comment') }
203
+
204
+ before do
205
+ comment_arthur.tags << medieval
206
+ comment_marco.tags << fantasy
207
+ end
208
+
209
+ it 'removes redundant joins from top query' do
210
+ s = Article.ransack(comments_tags_name_not_eq: "Fantasy")
211
+ sql = s.result.to_sql
212
+ expect(sql).to include('LEFT OUTER JOIN')
213
+ end
214
+
215
+ it 'handles != for single values' do
216
+ s = Article.ransack(comments_tags_name_not_eq: "Fantasy")
217
+ articles = s.result.to_a
218
+ expect(articles).to include marco
219
+ expect(articles).to_not include arthur
220
+ end
221
+
222
+ it 'handles NOT IN for multiple attributes' do
223
+ s = Article.ransack(comments_tags_name_not_in: ["Fantasy", "Scifi"])
224
+ articles = s.result.to_a
225
+
226
+ expect(articles).to include marco
227
+ expect(articles).to_not include arthur
228
+ end
229
+ end
230
+
231
+ context 'negative conditions on self-referenced associations' do
232
+ let(:pop) { Person.create!(name: 'Grandpa') }
233
+ let(:dad) { Person.create!(name: 'Father') }
234
+ let(:mom) { Person.create!(name: 'Mother') }
235
+ let(:son) { Person.create!(name: 'Grandchild') }
236
+
237
+ before do
238
+ son.parent = dad
239
+ dad.parent = pop
240
+ dad.children << son
241
+ mom.children << son
242
+ pop.children << dad
243
+ son.save! && dad.save! && mom.save! && pop.save!
244
+ end
245
+
246
+ it 'handles multiple associations and aliases' do
247
+ s = Person.ransack(
248
+ c: {
249
+ '0' => { a: ['name'], p: 'not_eq', v: ['Father'] },
250
+ '1' => {
251
+ a: ['children_name', 'parent_name'],
252
+ p: 'not_eq', v: ['Father'], m: 'or'
253
+ },
254
+ '2' => { a: ['children_salary'], p: 'eq', v: [nil] }
255
+ })
256
+ people = s.result
257
+
258
+ expect(people.to_a).to include son
259
+ expect(people.to_a).to include mom
260
+ expect(people.to_a).to_not include dad # rule '0': 'name'
261
+ expect(people.to_a).to_not include pop # rule '1': 'children_name'
262
+ end
263
+ end
264
+
265
+ describe '#ransack_alias' do
266
+ it 'translates an alias to the correct attributes' do
267
+ p = Person.create!(name: 'Meatloaf', email: 'babies@example.com')
268
+
269
+ s = Person.ransack(term_cont: 'atlo')
270
+ expect(s.result.to_a).to eq [p]
271
+
272
+ s = Person.ransack(term_cont: 'babi')
273
+ expect(s.result.to_a).to eq [p]
274
+
275
+ s = Person.ransack(term_cont: 'nomatch')
276
+ expect(s.result.to_a).to eq []
277
+ end
278
+
279
+ it 'also works with associations' do
280
+ dad = Person.create!(name: 'Birdman')
281
+ son = Person.create!(name: 'Weezy', parent: dad)
282
+
283
+ s = Person.ransack(daddy_eq: 'Birdman')
284
+ expect(s.result.to_a).to eq [son]
285
+
286
+ s = Person.ransack(daddy_eq: 'Drake')
287
+ expect(s.result.to_a).to eq []
288
+ end
289
+
290
+ it 'makes aliases available to subclasses' do
291
+ yngwie = Musician.create!(name: 'Yngwie Malmsteen')
292
+
293
+ musicians = Musician.ransack(term_cont: 'ngw').result
294
+ expect(musicians).to eq([yngwie])
295
+ end
296
+
297
+ it 'handles naming collisions gracefully' do
298
+ frank = Person.create!(name: 'Frank Stallone')
299
+
300
+ people = Person.ransack(term_cont: 'allon').result
301
+ expect(people).to eq([frank])
302
+
303
+ Class.new(Article) do
304
+ ransack_alias :term, :title
305
+ end
306
+
307
+ people = Person.ransack(term_cont: 'allon').result
308
+ expect(people).to eq([frank])
309
+ end
310
+ end
311
+
312
+ describe '#ransacker' do
313
+ # For infix tests
314
+ def self.sane_adapter?
315
+ case ::ActiveRecord::Base.connection.adapter_name
316
+ when 'SQLite3', 'PostgreSQL'
317
+ true
318
+ else
319
+ false
320
+ end
321
+ end
322
+ # in schema.rb, class Person:
323
+ # ransacker :reversed_name, formatter: proc { |v| v.reverse } do |parent|
324
+ # parent.table[:name]
325
+ # end
326
+ #
327
+ # ransacker :doubled_name do |parent|
328
+ # Arel::Nodes::InfixOperation.new(
329
+ # '||', parent.table[:name], parent.table[:name]
330
+ # )
331
+ # end
332
+
333
+ it 'creates ransack attributes' do
334
+ person = Person.create!(name: 'Aric Smith')
335
+
336
+ s = Person.ransack(reversed_name_eq: 'htimS cirA')
337
+ expect(s.result.size).to eq(1)
338
+
339
+ expect(s.result.first).to eq person
340
+ end
341
+
342
+ it 'can be accessed through associations' do
343
+ s = Person.ransack(children_reversed_name_eq: 'htimS cirA')
344
+ expect(s.result.to_sql).to match(
345
+ /#{quote_table_name("children_people")}.#{
346
+ quote_column_name("name")} = 'Aric Smith'/
347
+ )
348
+ end
349
+
350
+ it 'allows an attribute to be an InfixOperation' do
351
+ s = Person.ransack(doubled_name_eq: 'Aric SmithAric Smith')
352
+ expect(s.result.first).to eq Person.where(name: 'Aric Smith').first
353
+ end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?
354
+
355
+ it 'does not break #count if using InfixOperations' do
356
+ s = Person.ransack(doubled_name_eq: 'Aric SmithAric Smith')
357
+ expect(s.result.count).to eq 1
358
+ end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?
359
+
360
+ it 'should remove empty key value pairs from the params hash' do
361
+ s = Person.ransack(children_reversed_name_eq: '')
362
+ expect(s.result.to_sql).not_to match /LEFT OUTER JOIN/
363
+ end
364
+
365
+ it 'should keep proper key value pairs in the params hash' do
366
+ s = Person.ransack(children_reversed_name_eq: 'Testing')
367
+ expect(s.result.to_sql).to match /LEFT OUTER JOIN/
368
+ end
369
+
370
+ it 'should function correctly when nil is passed in' do
371
+ s = Person.ransack(nil)
372
+ end
373
+
374
+ it 'should function correctly when a blank string is passed in' do
375
+ s = Person.ransack('')
376
+ end
377
+
378
+ it 'should function correctly with a multi-parameter attribute' do
379
+ if ::ActiveRecord::VERSION::MAJOR >= 7
380
+ ::ActiveRecord.default_timezone = :utc
381
+ else
382
+ ::ActiveRecord::Base.default_timezone = :utc
383
+ end
384
+ Time.zone = 'UTC'
385
+
386
+ date = Date.current
387
+ s = Person.ransack(
388
+ { 'created_at_gteq(1i)' => date.year,
389
+ 'created_at_gteq(2i)' => date.month,
390
+ 'created_at_gteq(3i)' => date.day
391
+ }
392
+ )
393
+ expect(s.result.to_sql).to match />=/
394
+ expect(s.result.to_sql).to match date.to_s
395
+ end
396
+
397
+ it 'should function correctly when using fields with dots in them' do
398
+ s = Person.ransack(email_cont: 'example.com')
399
+ expect(s.result.exists?).to be true
400
+ end
401
+
402
+ it 'should function correctly when using fields with % in them' do
403
+ p = Person.create!(name: '110%-er')
404
+ s = Person.ransack(name_cont: '10%')
405
+ expect(s.result.to_a).to eq [p]
406
+ end
407
+
408
+ it 'should function correctly when using fields with backslashes in them' do
409
+ p = Person.create!(name: "\\WINNER\\")
410
+ s = Person.ransack(name_cont: "\\WINNER\\")
411
+ expect(s.result.to_a).to eq [p]
412
+ end
413
+
414
+ if ::ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::Base.respond_to?(:normalizes)
415
+ context 'with ActiveRecord::normalizes' do
416
+ around(:each) do |example|
417
+ # Create a temporary model class with normalization for testing
418
+ test_class = Class.new(ActiveRecord::Base) do
419
+ self.table_name = 'people'
420
+ normalizes :name, with: ->(name) { name.gsub(/[^a-z0-9]/, '_') }
421
+
422
+ def self.ransackable_attributes(auth_object = nil)
423
+ Person.ransackable_attributes(auth_object)
424
+ end
425
+
426
+ def self.name
427
+ 'TestPersonWithNormalization'
428
+ end
429
+ end
430
+
431
+ stub_const('TestPersonWithNormalization', test_class)
432
+ example.run
433
+ end
434
+
435
+ it 'should not apply normalization to LIKE wildcards for cont predicate' do
436
+ # Create a person with characters that would be normalized
437
+ p = TestPersonWithNormalization.create!(name: 'foo%bar')
438
+ expect(p.reload.name).to eq('foo_bar') # Verify normalization happened on storage
439
+
440
+ # Search should find the person using the original search term
441
+ s = TestPersonWithNormalization.ransack(name_cont: 'foo')
442
+ expect(s.result.to_a).to eq [p]
443
+
444
+ # Verify the SQL contains proper LIKE wildcards, not normalized ones
445
+ sql = s.result.to_sql
446
+ expect(sql).to include("LIKE '%foo%'")
447
+ expect(sql).not_to include("LIKE '_foo_'")
448
+ end
449
+
450
+ it 'should not apply normalization to LIKE wildcards for other LIKE predicates' do
451
+ p = TestPersonWithNormalization.create!(name: 'foo%bar')
452
+
453
+ # Test start predicate
454
+ s = TestPersonWithNormalization.ransack(name_start: 'foo')
455
+ expect(s.result.to_a).to eq [p]
456
+ expect(s.result.to_sql).to include("LIKE 'foo%'")
457
+
458
+ # Test end predicate
459
+ s = TestPersonWithNormalization.ransack(name_end: 'bar')
460
+ expect(s.result.to_a).to eq [p]
461
+ expect(s.result.to_sql).to include("LIKE '%bar'")
462
+
463
+ # Test i_cont predicate
464
+ s = TestPersonWithNormalization.ransack(name_i_cont: 'FOO')
465
+ expect(s.result.to_a).to eq [p]
466
+ expect(s.result.to_sql).to include("LIKE '%foo%'")
467
+ end
468
+ end
469
+ end
470
+
471
+ context 'searching by underscores' do
472
+ # when escaping is supported right in LIKE expression without adding extra expressions
473
+ def self.simple_escaping?
474
+ case ::ActiveRecord::Base.connection.adapter_name
475
+ when 'Mysql2', 'PostgreSQL'
476
+ true
477
+ else
478
+ false
479
+ end
480
+ end
481
+
482
+ it 'should search correctly if matches exist' do
483
+ p = Person.create!(name: 'name_with_underscore')
484
+ s = Person.ransack(name_cont: 'name_')
485
+ expect(s.result.to_a).to eq [p]
486
+ end if simple_escaping?
487
+
488
+ it 'should return empty result if no matches' do
489
+ Person.create!(name: 'name_with_underscore')
490
+ s = Person.ransack(name_cont: 'n_')
491
+ expect(s.result.to_a).to eq []
492
+ end if simple_escaping?
493
+ end
494
+
495
+ context 'searching on an `in` predicate with a ransacker' do
496
+ it 'should function correctly when passing an array of ids' do
497
+ s = Person.ransack(array_people_ids_in: true)
498
+ expect(s.result.count).to be > 0
499
+
500
+ s = Person.ransack(array_where_people_ids_in: [1, '2', 3])
501
+ expect(s.result.count).to be 3
502
+ expect(s.result.map(&:id)).to eq [3, 2, 1]
503
+ end
504
+
505
+ it 'should function correctly with HABTM associations' do
506
+ article = Article.first
507
+ tag = article.tags.first
508
+ s = Person.ransack(article_tags_in: [tag.id])
509
+
510
+ expect(s.result.count).to be 1
511
+ expect(s.result.map(&:id)).to eq [article.person.id]
512
+ end
513
+
514
+ it 'should function correctly when passing an array of strings' do
515
+ a, b = Person.select(:id).order(:id).limit(2).map { |a| a.id.to_s }
516
+
517
+ Person.create!(name: a)
518
+ s = Person.ransack(array_people_names_in: true)
519
+ expect(s.result.count).to be > 0
520
+ s = Person.ransack(array_where_people_names_in: a)
521
+ expect(s.result.count).to be 1
522
+
523
+ Person.create!(name: b)
524
+ s = Person.ransack(array_where_people_names_in: [a, b])
525
+ expect(s.result.count).to be 2
526
+ end
527
+
528
+ it 'should function correctly with an Arel SqlLiteral' do
529
+ s = Person.ransack(sql_literal_id_in: 1)
530
+ expect(s.result.count).to be 1
531
+ s = Person.ransack(sql_literal_id_in: ['2', 4, '5', 8])
532
+ expect(s.result.count).to be 4
533
+ end
534
+ end
535
+
536
+ context 'search on an `in` predicate with an array' do
537
+ it 'should function correctly when passing an array of ids' do
538
+ array = Person.all.map(&:id)
539
+ s = Person.ransack(id_in: array)
540
+ expect(s.result.count).to eq array.size
541
+ end
542
+ end
543
+
544
+ it 'should work correctly when an attribute name ends with _start' do
545
+ p = Person.create!(new_start: 'Bar and foo', name: 'Xiang')
546
+
547
+ s = Person.ransack(new_start_end: ' and foo')
548
+ expect(s.result.to_a).to eq [p]
549
+
550
+ s = Person.ransack(name_or_new_start_start: 'Xia')
551
+ expect(s.result.to_a).to eq [p]
552
+
553
+ s = Person.ransack(new_start_or_name_end: 'iang')
554
+ expect(s.result.to_a).to eq [p]
555
+ end
556
+
557
+ it 'should work correctly when an attribute name ends with _end' do
558
+ p = Person.create!(stop_end: 'Foo and bar', name: 'Marianne')
559
+
560
+ s = Person.ransack(stop_end_start: 'Foo and')
561
+ expect(s.result.to_a).to eq [p]
562
+
563
+ s = Person.ransack(stop_end_or_name_end: 'anne')
564
+ expect(s.result.to_a).to eq [p]
565
+
566
+ s = Person.ransack(name_or_stop_end_end: ' bar')
567
+ expect(s.result.to_a).to eq [p]
568
+ end
569
+
570
+ it 'should work correctly when an attribute name has `and` in it' do
571
+ p = Person.create!(terms_and_conditions: true)
572
+ s = Person.ransack(terms_and_conditions_eq: true)
573
+ expect(s.result.to_a).to eq [p]
574
+ end
575
+
576
+ context 'attribute aliased column names',
577
+ if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
578
+ it 'should be translated to original column name' do
579
+ s = Person.ransack(full_name_eq: 'Nicolas Cage')
580
+ expect(s.result.to_sql).to match(
581
+ /WHERE #{quote_table_name("people")}.#{quote_column_name("name")}/
582
+ )
583
+ end
584
+
585
+ it 'should translate on associations' do
586
+ s = Person.ransack(articles_content_cont: 'Nicolas Cage')
587
+ expect(s.result.to_sql).to match(
588
+ /#{quote_table_name("articles")}.#{
589
+ quote_column_name("body")} I?LIKE '%Nicolas Cage%'/
590
+ )
591
+ end
592
+ end
593
+
594
+ it 'sorts with different join variants' do
595
+ comments = [
596
+ Comment.create(article: Article.create(title: 'Avenger'), person: Person.create(salary: 100_000)),
597
+ Comment.create(article: Article.create(title: 'Avenge'), person: Person.create(salary: 50_000)),
598
+ ]
599
+ expect(Comment.ransack(article_title_cont: 'aven', s: 'person_salary desc').result).to eq(comments)
600
+ expect(Comment.joins(:person).ransack(s: 'persons_salarydesc', article_title_cont: 'aven').result).to eq(comments)
601
+ expect(Comment.joins(:person).ransack(article_title_cont: 'aven', s: 'persons_salary desc').result).to eq(comments)
602
+ end
603
+
604
+ it 'allows sort by `only_sort` field' do
605
+ s = Person.ransack(
606
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_sort' } }
607
+ )
608
+ expect(s.result.to_sql).to match(
609
+ /ORDER BY #{quote_table_name("people")}.#{
610
+ quote_column_name("only_sort")} ASC/
611
+ )
612
+ end
613
+
614
+ it 'does not sort by `only_search` field' do
615
+ s = Person.ransack(
616
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_search' } }
617
+ )
618
+ expect(s.result.to_sql).not_to match(
619
+ /ORDER BY #{quote_table_name("people")}.#{
620
+ quote_column_name("only_search")} ASC/
621
+ )
622
+ end
623
+
624
+ it 'allows search by `only_search` field' do
625
+ s = Person.ransack(only_search_eq: 'htimS cirA')
626
+ expect(s.result.to_sql).to match(
627
+ /WHERE #{quote_table_name("people")}.#{
628
+ quote_column_name("only_search")} = 'htimS cirA'/
629
+ )
630
+ end
631
+
632
+ it 'cannot be searched by `only_sort`' do
633
+ s = Person.ransack(only_sort_eq: 'htimS cirA')
634
+ expect(s.result.to_sql).not_to match(
635
+ /WHERE #{quote_table_name("people")}.#{
636
+ quote_column_name("only_sort")} = 'htimS cirA'/
637
+ )
638
+ end
639
+
640
+ it 'allows sort by `only_admin` field, if auth_object: :admin' do
641
+ s = Person.ransack(
642
+ { 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_admin' } } },
643
+ { auth_object: :admin }
644
+ )
645
+ expect(s.result.to_sql).to match(
646
+ /ORDER BY #{quote_table_name("people")}.#{
647
+ quote_column_name("only_admin")} ASC/
648
+ )
649
+ end
650
+
651
+ it 'does not sort by `only_admin` field, if auth_object: nil' do
652
+ s = Person.ransack(
653
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_admin' } }
654
+ )
655
+ expect(s.result.to_sql).not_to match(
656
+ /ORDER BY #{quote_table_name("people")}.#{
657
+ quote_column_name("only_admin")} ASC/
658
+ )
659
+ end
660
+
661
+ it 'allows search by `only_admin` field, if auth_object: :admin' do
662
+ s = Person.ransack(
663
+ { only_admin_eq: 'htimS cirA' },
664
+ { auth_object: :admin }
665
+ )
666
+ expect(s.result.to_sql).to match(
667
+ /WHERE #{quote_table_name("people")}.#{
668
+ quote_column_name("only_admin")} = 'htimS cirA'/
669
+ )
670
+ end
671
+
672
+ it 'cannot be searched by `only_admin`, if auth_object: nil' do
673
+ s = Person.ransack(only_admin_eq: 'htimS cirA')
674
+ expect(s.result.to_sql).not_to match(
675
+ /WHERE #{quote_table_name("people")}.#{
676
+ quote_column_name("only_admin")} = 'htimS cirA'/
677
+ )
678
+ end
679
+
680
+ it 'should allow passing ransacker arguments to a ransacker' do
681
+ s = Person.ransack(
682
+ c: [{
683
+ a: {
684
+ '0' => {
685
+ name: 'with_arguments', ransacker_args: [10, 100]
686
+ }
687
+ },
688
+ p: 'cont',
689
+ v: ['Passing arguments to ransackers!']
690
+ }]
691
+ )
692
+ expect(s.result.to_sql).to match(
693
+ /LENGTH\(articles.body\) BETWEEN 10 AND 100/
694
+ )
695
+ expect(s.result.to_sql).to match(
696
+ /LIKE \'\%Passing arguments to ransackers!\%\'/
697
+ )
698
+ expect { s.result.first }.to_not raise_error
699
+ end
700
+
701
+ it 'should allow sort passing arguments to a ransacker' do
702
+ s = Person.ransack(
703
+ s: {
704
+ '0' => {
705
+ name: 'with_arguments', dir: 'desc', ransacker_args: [2, 6]
706
+ }
707
+ }
708
+ )
709
+ expect(s.result.to_sql).to match(
710
+ /ORDER BY \(SELECT MAX\(articles.title\) FROM articles/
711
+ )
712
+ expect(s.result.to_sql).to match(
713
+ /WHERE articles.person_id = people.id AND LENGTH\(articles.body\)/
714
+ )
715
+ expect(s.result.to_sql).to match(
716
+ /BETWEEN 2 AND 6 GROUP BY articles.person_id \) DESC/
717
+ )
718
+ end
719
+
720
+ context 'case insensitive sorting' do
721
+ it 'allows sort by desc' do
722
+ search = Person.ransack(sorts: ['name_case_insensitive desc'])
723
+ expect(search.result.to_sql).to match /ORDER BY LOWER(.*) DESC/
724
+ end
725
+
726
+ it 'allows sort by asc' do
727
+ search = Person.ransack(sorts: ['name_case_insensitive asc'])
728
+ expect(search.result.to_sql).to match /ORDER BY LOWER(.*) ASC/
729
+ end
730
+ end
731
+
732
+ context 'ransacker with different types' do
733
+ it 'handles string type ransacker correctly' do
734
+ s = Person.ransack(name_case_insensitive_eq: 'test')
735
+ expect(s.result.to_sql).to match(/LOWER\(.*\) = 'test'/)
736
+ end
737
+
738
+ it 'handles integer type ransacker correctly' do
739
+ s = Person.ransack(sql_literal_id_eq: 1)
740
+ expect(s.result.to_sql).to match(/people\.id = 1/)
741
+ end
742
+ end
743
+
744
+ context 'ransacker with formatter returning nil' do
745
+ it 'handles formatter returning nil gracefully' do
746
+ # This tests the edge case where a formatter might return nil
747
+ s = Person.ransack(article_tags_eq: 999999) # Non-existent tag ID
748
+ expect { s.result.to_sql }.not_to raise_error
749
+ end
750
+ end
751
+
752
+ context 'ransacker with array formatters' do
753
+ it 'handles array_people_ids formatter correctly' do
754
+ person1 = Person.create!(name: 'Test1')
755
+ person2 = Person.create!(name: 'Test2')
756
+
757
+ s = Person.ransack(array_people_ids_eq: 'test')
758
+ expect { s.result }.not_to raise_error
759
+ end
760
+
761
+ it 'handles array_where_people_ids formatter correctly' do
762
+ person1 = Person.create!(name: 'Test1')
763
+ person2 = Person.create!(name: 'Test2')
764
+
765
+ s = Person.ransack(array_where_people_ids_eq: [person1.id, person2.id])
766
+ expect { s.result }.not_to raise_error
767
+ end
768
+ end
769
+
770
+ context 'regular sorting' do
771
+ it 'allows sort by desc' do
772
+ search = Person.ransack(sorts: ['name desc'])
773
+ expect(search.result.to_sql).to match /ORDER BY .* DESC/
774
+ end
775
+
776
+ it 'allows sort by asc' do
777
+ search = Person.ransack(sorts: ['name asc'])
778
+ expect(search.result.to_sql).to match /ORDER BY .* ASC/
779
+ end
780
+ end
781
+
782
+ context 'sorting by a scope' do
783
+ it 'applies the correct scope' do
784
+ search = Person.ransack(sorts: ['reverse_name asc'])
785
+ expect(search.result.to_sql).to include("ORDER BY REVERSE(name) ASC")
786
+ end
787
+ end
788
+ end
789
+
790
+ describe '#ransackable_attributes' do
791
+ context 'when auth_object is nil' do
792
+ subject { Person.ransackable_attributes }
793
+
794
+ it { should include 'name' }
795
+ it { should include 'reversed_name' }
796
+ it { should include 'doubled_name' }
797
+ it { should include 'term' }
798
+ it { should include 'only_search' }
799
+ it { should_not include 'only_sort' }
800
+ it { should_not include 'only_admin' }
801
+
802
+ if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
803
+ it { should include 'full_name' }
804
+ end
805
+ end
806
+
807
+ context 'with auth_object :admin' do
808
+ subject { Person.ransackable_attributes(:admin) }
809
+
810
+ it { should include 'name' }
811
+ it { should include 'reversed_name' }
812
+ it { should include 'doubled_name' }
813
+ it { should include 'only_search' }
814
+ it { should_not include 'only_sort' }
815
+ it { should include 'only_admin' }
816
+ end
817
+
818
+ context 'when not defined in model, nor in ApplicationRecord' do
819
+ subject { Article.ransackable_attributes }
820
+
821
+ it "raises a helpful error" do
822
+ without_application_record_method(:ransackable_attributes) do
823
+ expect { subject }.to raise_error(RuntimeError, /Ransack needs Article attributes explicitly allowlisted/)
824
+ end
825
+ end
826
+ end
827
+
828
+ context 'when defined only in model by delegating to super' do
829
+ subject { Article.ransackable_attributes }
830
+
831
+ around do |example|
832
+ Article.singleton_class.define_method(:ransackable_attributes) do
833
+ super(nil) - super(nil)
834
+ end
835
+
836
+ example.run
837
+ ensure
838
+ Article.singleton_class.remove_method(:ransackable_attributes)
839
+ end
840
+
841
+ it "returns the allowlist in the model, and warns" do
842
+ without_application_record_method(:ransackable_attributes) do
843
+ expect { subject }.to output(/Ransack's builtin `ransackable_attributes` method is deprecated/).to_stderr
844
+ expect(subject).to be_empty
845
+ end
846
+ end
847
+ end
848
+ end
849
+
850
+ describe '#ransortable_attributes' do
851
+ context 'when auth_object is nil' do
852
+ subject { Person.ransortable_attributes }
853
+
854
+ it { should include 'name' }
855
+ it { should include 'reversed_name' }
856
+ it { should include 'doubled_name' }
857
+ it { should include 'only_sort' }
858
+ it { should_not include 'only_search' }
859
+ it { should_not include 'only_admin' }
860
+ end
861
+
862
+ context 'with auth_object :admin' do
863
+ subject { Person.ransortable_attributes(:admin) }
864
+
865
+ it { should include 'name' }
866
+ it { should include 'reversed_name' }
867
+ it { should include 'doubled_name' }
868
+ it { should include 'only_sort' }
869
+ it { should_not include 'only_search' }
870
+ it { should include 'only_admin' }
871
+ end
872
+ end
873
+
874
+ describe '#ransackable_associations' do
875
+ subject { Person.ransackable_associations }
876
+
877
+ it { should include 'parent' }
878
+ it { should include 'children' }
879
+ it { should include 'articles' }
880
+
881
+ context 'when not defined in model, nor in ApplicationRecord' do
882
+ subject { Article.ransackable_associations }
883
+
884
+ it "raises a helpful error" do
885
+ without_application_record_method(:ransackable_associations) do
886
+ expect { subject }.to raise_error(RuntimeError, /Ransack needs Article associations explicitly allowlisted/)
887
+ end
888
+ end
889
+ end
890
+
891
+ context 'when defined only in model by delegating to super' do
892
+ subject { Article.ransackable_associations }
893
+
894
+ around do |example|
895
+ Article.singleton_class.define_method(:ransackable_associations) do
896
+ super(nil) - super(nil)
897
+ end
898
+
899
+ example.run
900
+ ensure
901
+ Article.singleton_class.remove_method(:ransackable_associations)
902
+ end
903
+
904
+ it "returns the allowlist in the model, and warns" do
905
+ without_application_record_method(:ransackable_associations) do
906
+ expect { subject }.to output(/Ransack's builtin `ransackable_associations` method is deprecated/).to_stderr
907
+ expect(subject).to be_empty
908
+ end
909
+ end
910
+ end
911
+ end
912
+
913
+ describe '#ransackable_scopes' do
914
+ subject { Person.ransackable_scopes }
915
+
916
+ it { should eq [] }
917
+ end
918
+
919
+ describe '#ransackable_scopes_skip_sanitize_args' do
920
+ subject { Person.ransackable_scopes_skip_sanitize_args }
921
+
922
+ it { should eq [] }
923
+ end
924
+
925
+ private
926
+
927
+ def without_application_record_method(method)
928
+ ApplicationRecord.singleton_class.alias_method :"original_#{method}", :"#{method}"
929
+ ApplicationRecord.singleton_class.remove_method :"#{method}"
930
+
931
+ yield
932
+ ensure
933
+ ApplicationRecord.singleton_class.alias_method :"#{method}", :"original_#{method}"
934
+ ApplicationRecord.singleton_class.remove_method :"original_#{method}"
935
+ end
936
+
937
+ def rails7_and_mysql
938
+ ::ActiveRecord::VERSION::MAJOR >= 7 && ENV['DB'] == 'mysql'
939
+ end
940
+ end
941
+ end
942
+ end
943
+ end