ransack 4.1.1 → 4.4.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -3
  3. data/lib/polyamorous/activerecord/join_association_7_2.rb +55 -0
  4. data/lib/polyamorous/polyamorous.rb +5 -1
  5. data/lib/ransack/adapters/active_record/context.rb +32 -5
  6. data/lib/ransack/constants.rb +1 -1
  7. data/lib/ransack/context.rb +7 -4
  8. data/lib/ransack/helpers/form_builder.rb +6 -7
  9. data/lib/ransack/helpers/form_helper.rb +86 -20
  10. data/lib/ransack/invalid_search_error.rb +3 -0
  11. data/lib/ransack/locale/ja.yml +51 -51
  12. data/lib/ransack/locale/ko.yml +70 -0
  13. data/lib/ransack/locale/uk.yml +72 -0
  14. data/lib/ransack/nodes/condition.rb +39 -7
  15. data/lib/ransack/nodes/grouping.rb +1 -1
  16. data/lib/ransack/nodes/sort.rb +1 -1
  17. data/lib/ransack/nodes/value.rb +9 -1
  18. data/lib/ransack/search.rb +4 -3
  19. data/lib/ransack/version.rb +1 -1
  20. data/lib/ransack.rb +9 -0
  21. data/spec/polyamorous/join_association_spec.rb +0 -1
  22. data/spec/polyamorous/join_dependency_spec.rb +0 -1
  23. data/spec/ransack/adapters/active_record/base_spec.rb +106 -3
  24. data/spec/ransack/adapters/active_record/context_spec.rb +72 -0
  25. data/spec/ransack/helpers/form_builder_spec.rb +0 -2
  26. data/spec/ransack/helpers/form_helper_spec.rb +219 -5
  27. data/spec/ransack/nodes/condition_spec.rb +230 -0
  28. data/spec/ransack/nodes/grouping_spec.rb +2 -2
  29. data/spec/ransack/nodes/value_spec.rb +12 -1
  30. data/spec/ransack/predicate_spec.rb +16 -9
  31. data/spec/ransack/search_spec.rb +121 -1
  32. data/spec/ransack/translate_spec.rb +0 -1
  33. data/spec/spec_helper.rb +2 -3
  34. data/spec/support/schema.rb +42 -0
  35. metadata +17 -86
  36. data/.github/FUNDING.yml +0 -3
  37. data/.github/SECURITY.md +0 -12
  38. data/.github/workflows/codeql.yml +0 -72
  39. data/.github/workflows/cronjob.yml +0 -99
  40. data/.github/workflows/deploy.yml +0 -35
  41. data/.github/workflows/rubocop.yml +0 -20
  42. data/.github/workflows/test-deploy.yml +0 -29
  43. data/.github/workflows/test.yml +0 -131
  44. data/.gitignore +0 -7
  45. data/.nojekyll +0 -0
  46. data/.rubocop.yml +0 -50
  47. data/CHANGELOG.md +0 -1176
  48. data/CONTRIBUTING.md +0 -171
  49. data/Gemfile +0 -53
  50. data/Rakefile +0 -24
  51. data/bug_report_templates/test-ransack-scope-and-column-same-name.rb +0 -78
  52. data/bug_report_templates/test-ransacker-arel-present-predicate.rb +0 -75
  53. data/docs/.gitignore +0 -19
  54. data/docs/.nojekyll +0 -0
  55. data/docs/babel.config.js +0 -3
  56. data/docs/blog/2022-03-27-ransack-3.0.0.md +0 -20
  57. data/docs/docs/getting-started/_category_.json +0 -4
  58. data/docs/docs/getting-started/advanced-mode.md +0 -46
  59. data/docs/docs/getting-started/configuration.md +0 -47
  60. data/docs/docs/getting-started/search-matches.md +0 -67
  61. data/docs/docs/getting-started/simple-mode.md +0 -288
  62. data/docs/docs/getting-started/sorting.md +0 -71
  63. data/docs/docs/getting-started/using-predicates.md +0 -282
  64. data/docs/docs/going-further/_category_.json +0 -4
  65. data/docs/docs/going-further/acts-as-taggable-on.md +0 -114
  66. data/docs/docs/going-further/associations.md +0 -70
  67. data/docs/docs/going-further/custom-predicates.md +0 -52
  68. data/docs/docs/going-further/documentation.md +0 -43
  69. data/docs/docs/going-further/exporting-to-csv.md +0 -49
  70. data/docs/docs/going-further/external-guides.md +0 -57
  71. data/docs/docs/going-further/form-customisation.md +0 -63
  72. data/docs/docs/going-further/i18n.md +0 -53
  73. data/docs/docs/going-further/img/create_release.png +0 -0
  74. data/docs/docs/going-further/merging-searches.md +0 -41
  75. data/docs/docs/going-further/other-notes.md +0 -428
  76. data/docs/docs/going-further/polymorphic-search.md +0 -46
  77. data/docs/docs/going-further/ransackers.md +0 -331
  78. data/docs/docs/going-further/release_process.md +0 -36
  79. data/docs/docs/going-further/saving-queries.md +0 -82
  80. data/docs/docs/going-further/searching-postgres.md +0 -57
  81. data/docs/docs/going-further/wiki-contributors.md +0 -82
  82. data/docs/docs/intro.md +0 -99
  83. data/docs/docusaurus.config.js +0 -120
  84. data/docs/package.json +0 -42
  85. data/docs/sidebars.js +0 -31
  86. data/docs/src/components/HomepageFeatures/index.js +0 -64
  87. data/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  88. data/docs/src/css/custom.css +0 -39
  89. data/docs/src/pages/index.module.css +0 -23
  90. data/docs/src/pages/markdown-page.md +0 -7
  91. data/docs/static/.nojekyll +0 -0
  92. data/docs/static/img/docusaurus.png +0 -0
  93. data/docs/static/img/favicon.ico +0 -0
  94. data/docs/static/img/logo.svg +0 -1
  95. data/docs/static/img/tutorial/docsVersionDropdown.png +0 -0
  96. data/docs/static/img/tutorial/localeDropdown.png +0 -0
  97. data/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  98. data/docs/static/img/undraw_docusaurus_react.svg +0 -170
  99. data/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  100. data/docs/static/logo/ransack-h.png +0 -0
  101. data/docs/static/logo/ransack-h.svg +0 -34
  102. data/docs/static/logo/ransack-v.png +0 -0
  103. data/docs/static/logo/ransack-v.svg +0 -34
  104. data/docs/static/logo/ransack.png +0 -0
  105. data/docs/static/logo/ransack.svg +0 -21
  106. data/docs/yarn.lock +0 -8879
  107. data/ransack.gemspec +0 -26
@@ -3,7 +3,6 @@ require 'spec_helper'
3
3
  module Ransack
4
4
  module Helpers
5
5
  describe FormHelper do
6
-
7
6
  router = ActionDispatch::Routing::RouteSet.new
8
7
  router.draw do
9
8
  resources :people, :notes
@@ -458,9 +457,7 @@ module Ransack
458
457
  end
459
458
 
460
459
  context 'view has existing parameters' do
461
-
462
460
  describe '#sort_link should not remove existing params' do
463
-
464
461
  before { @controller.view_context.params[:exist] = 'existing' }
465
462
 
466
463
  subject {
@@ -478,7 +475,6 @@ module Ransack
478
475
  end
479
476
 
480
477
  describe '#sort_url should not remove existing params' do
481
-
482
478
  before { @controller.view_context.params[:exist] = 'existing' }
483
479
 
484
480
  subject {
@@ -496,12 +492,12 @@ module Ransack
496
492
  end
497
493
 
498
494
  context 'using a real ActionController::Parameter object' do
499
-
500
495
  describe 'with symbol q:, #sort_link should include search params' do
501
496
  subject { @controller.view_context.sort_link(Person.ransack, :name) }
502
497
  let(:params) { ActionController::Parameters.new(
503
498
  { q: { name_eq: 'TEST' }, controller: 'people' }
504
499
  ) }
500
+
505
501
  before { @controller.instance_variable_set(:@params, params) }
506
502
 
507
503
  it {
@@ -517,6 +513,7 @@ module Ransack
517
513
  let(:params) { ActionController::Parameters.new(
518
514
  { q: { name_eq: 'TEST' }, controller: 'people' }
519
515
  ) }
516
+
520
517
  before { @controller.instance_variable_set(:@params, params) }
521
518
 
522
519
  it {
@@ -533,6 +530,7 @@ module Ransack
533
530
  ActionController::Parameters.new(
534
531
  { 'q' => { name_eq: 'Test2' }, controller: 'people' }
535
532
  ) }
533
+
536
534
  before { @controller.instance_variable_set(:@params, params) }
537
535
 
538
536
  it {
@@ -549,6 +547,7 @@ module Ransack
549
547
  ActionController::Parameters.new(
550
548
  { 'q' => { name_eq: 'Test2' }, controller: 'people' }
551
549
  ) }
550
+
552
551
  before { @controller.instance_variable_set(:@params, params) }
553
552
 
554
553
  it {
@@ -850,12 +849,227 @@ module Ransack
850
849
  before do
851
850
  Ransack.configure { |c| c.search_key = :example }
852
851
  end
852
+ after do
853
+ Ransack.configure { |c| c.search_key = :q }
854
+ end
853
855
  subject {
854
856
  @controller.view_context
855
857
  .search_form_for(Person.ransack) { |f| f.text_field :name_eq }
856
858
  }
857
859
  it { should match /example_name_eq/ }
858
860
  end
861
+
862
+ describe '#search_form_with with default format' do
863
+ subject { @controller.view_context
864
+ .search_form_with(model: Person.ransack) {} }
865
+ it { should match /action="\/people"/ }
866
+ end
867
+
868
+ describe '#search_form_with with pdf format' do
869
+ subject {
870
+ @controller.view_context
871
+ .search_form_with(model: Person.ransack, format: :pdf) {}
872
+ }
873
+ it { should match /action="\/people.pdf"/ }
874
+ end
875
+
876
+ describe '#search_form_with with json format' do
877
+ subject {
878
+ @controller.view_context
879
+ .search_form_with(model: Person.ransack, format: :json) {}
880
+ }
881
+ it { should match /action="\/people.json"/ }
882
+ end
883
+
884
+ describe '#search_form_with with an array of routes' do
885
+ subject {
886
+ @controller.view_context
887
+ .search_form_with(model: [:admin, Comment.ransack]) {}
888
+ }
889
+ it { should match /action="\/admin\/comments"/ }
890
+ end
891
+
892
+ describe '#search_form_with with custom default search key' do
893
+ before do
894
+ Ransack.configure { |c| c.search_key = :example }
895
+ end
896
+ after do
897
+ Ransack.configure { |c| c.search_key = :q }
898
+ end
899
+ subject {
900
+ @controller.view_context
901
+ .search_form_with(model: Person.ransack) { |f| f.text_field :name_eq }
902
+ }
903
+ it { should match /example\[name_eq\]/ }
904
+ end
905
+
906
+ describe '#search_form_with without Ransack::Search object' do
907
+ it 'raises ArgumentError' do
908
+ expect {
909
+ @controller.view_context.search_form_with(model: "not a search object") {}
910
+ }.to raise_error(ArgumentError, 'No Ransack::Search object was provided to search_form_with!')
911
+ end
912
+ end
913
+
914
+ describe '#turbo_search_form_for with default options' do
915
+ subject {
916
+ @controller.view_context
917
+ .turbo_search_form_for(Person.ransack) {}
918
+ }
919
+ it { should match /action="\/people"/ }
920
+ it { should match /method="post"/ }
921
+ it { should match /data-turbo-action="advance"/ }
922
+ end
923
+
924
+ describe '#turbo_search_form_for with custom method' do
925
+ subject {
926
+ @controller.view_context
927
+ .turbo_search_form_for(Person.ransack, method: :patch) {}
928
+ }
929
+ it { should match /method="post"/ }
930
+ it { should match /name="_method" value="patch"/ }
931
+ it { should match /data-turbo-action="advance"/ }
932
+ end
933
+
934
+ describe '#turbo_search_form_for with turbo_frame' do
935
+ subject {
936
+ @controller.view_context
937
+ .turbo_search_form_for(Person.ransack, turbo_frame: 'search_results') {}
938
+ }
939
+ it { should match /data-turbo-frame="search_results"/ }
940
+ end
941
+
942
+ describe '#turbo_search_form_for with custom turbo_action' do
943
+ subject {
944
+ @controller.view_context
945
+ .turbo_search_form_for(Person.ransack, turbo_action: 'replace') {}
946
+ }
947
+ it { should match /data-turbo-action="replace"/ }
948
+ end
949
+
950
+ describe '#turbo_search_form_for with format' do
951
+ subject {
952
+ @controller.view_context
953
+ .turbo_search_form_for(Person.ransack, format: :json) {}
954
+ }
955
+ it { should match /action="\/people.json"/ }
956
+ end
957
+
958
+ describe '#turbo_search_form_for with array of routes' do
959
+ subject {
960
+ @controller.view_context
961
+ .turbo_search_form_for([:admin, Comment.ransack]) {}
962
+ }
963
+ it { should match /action="\/admin\/comments"/ }
964
+ end
965
+
966
+ describe '#turbo_search_form_for with custom search key' do
967
+ before do
968
+ Ransack.configure { |c| c.search_key = :example }
969
+ end
970
+ after do
971
+ Ransack.configure { |c| c.search_key = :q }
972
+ end
973
+ subject {
974
+ @controller.view_context
975
+ .turbo_search_form_for(Person.ransack) { |f| f.text_field :name_eq }
976
+ }
977
+ it { should match /example_name_eq/ }
978
+ end
979
+
980
+ describe '#turbo_search_form_for without Ransack::Search object' do
981
+ it 'raises ArgumentError' do
982
+ expect {
983
+ @controller.view_context.turbo_search_form_for("not a search object") {}
984
+ }.to raise_error(ArgumentError, 'No Ransack::Search object was provided to turbo_search_form_for!')
985
+ end
986
+ end
987
+
988
+ describe 'private helper methods' do
989
+ let(:helper) { @controller.view_context }
990
+ let(:search) { Person.ransack }
991
+
992
+ describe '#build_turbo_options' do
993
+ it 'builds turbo options with frame' do
994
+ options = { turbo_frame: 'results', turbo_action: 'replace' }
995
+ result = helper.send(:build_turbo_options, options)
996
+ expect(result).to eq({
997
+ data: {
998
+ turbo_frame: 'results',
999
+ turbo_action: 'replace'
1000
+ }
1001
+ })
1002
+ expect(options).to be_empty
1003
+ end
1004
+
1005
+ it 'builds turbo options without frame' do
1006
+ options = { turbo_action: 'advance' }
1007
+ result = helper.send(:build_turbo_options, options)
1008
+ expect(result).to eq({ data: { turbo_action: 'advance' } })
1009
+ end
1010
+
1011
+ it 'uses default turbo action' do
1012
+ options = {}
1013
+ result = helper.send(:build_turbo_options, options)
1014
+ expect(result).to eq({ data: { turbo_action: 'advance' } })
1015
+ end
1016
+ end
1017
+
1018
+ describe '#build_html_options' do
1019
+ it 'builds HTML options with correct method' do
1020
+ options = { class: 'custom' }
1021
+ result = helper.send(:build_html_options, search, options, :post)
1022
+ expect(result[:method]).to eq(:post)
1023
+ expect(result[:class]).to include('custom')
1024
+ end
1025
+ end
1026
+
1027
+ describe '#extract_search_and_set_url' do
1028
+ it 'extracts search from Ransack::Search object' do
1029
+ options = {}
1030
+ result = helper.send(:extract_search_and_set_url, search, options, 'search_form_for')
1031
+ expect(result).to eq(search)
1032
+ expect(options[:url]).to match(/people/)
1033
+ end
1034
+
1035
+ it 'extracts search from array with Search object' do
1036
+ options = {}
1037
+ comment_search = Comment.ransack
1038
+ result = helper.send(:extract_search_and_set_url, [:admin, comment_search], options, 'search_form_for')
1039
+ expect(result).to eq(comment_search)
1040
+ expect(options[:url]).to match(/admin/)
1041
+ end
1042
+
1043
+ it 'raises error for invalid record with correct method name' do
1044
+ options = {}
1045
+ expect {
1046
+ helper.send(:extract_search_and_set_url, "invalid", options, 'turbo_search_form_for')
1047
+ }.to raise_error(ArgumentError, 'No Ransack::Search object was provided to turbo_search_form_for!')
1048
+ end
1049
+
1050
+ it 'extracts search from Ransack::Search object for search_form_with' do
1051
+ options = {}
1052
+ result = helper.send(:extract_search_and_set_url, search, options, 'search_form_with')
1053
+ expect(result).to eq(search)
1054
+ expect(options[:url]).to match(/people/)
1055
+ end
1056
+
1057
+ it 'extracts search from array with Search object for search_form_with' do
1058
+ options = {}
1059
+ comment_search = Comment.ransack
1060
+ result = helper.send(:extract_search_and_set_url, [:admin, comment_search], options, 'search_form_with')
1061
+ expect(result).to eq(comment_search)
1062
+ expect(options[:url]).to match(/admin/)
1063
+ end
1064
+
1065
+ it 'raises error for invalid record with correct method name for search_form_with' do
1066
+ options = {}
1067
+ expect {
1068
+ helper.send(:extract_search_and_set_url, "invalid", options, 'search_form_with')
1069
+ }.to raise_error(ArgumentError, 'No Ransack::Search object was provided to search_form_with!')
1070
+ end
1071
+ end
1072
+ end
859
1073
  end
860
1074
  end
861
1075
  end
@@ -64,6 +64,7 @@ module Ransack
64
64
  end
65
65
 
66
66
  specify { expect { subject }.to raise_error ArgumentError }
67
+ specify { expect { subject }.to raise_error InvalidSearchError }
67
68
  end
68
69
 
69
70
  context "when ignore_unknown_conditions is true" do
@@ -98,6 +99,235 @@ module Ransack
98
99
  specify { expect(subject).to eq Condition.extract(Context.for(Person), 'full_name_eq', Person.first.name) }
99
100
  end
100
101
  end
102
+
103
+ context 'with wildcard string values' do
104
+ it 'properly quotes values with wildcards for LIKE predicates' do
105
+ ransack_hash = { name_cont: 'test%' }
106
+ sql = Person.ransack(ransack_hash).result.to_sql
107
+
108
+ # The % should be properly quoted in the SQL
109
+ case ActiveRecord::Base.connection.adapter_name
110
+ when "Mysql2"
111
+ expect(sql).to include("LIKE '%test\\\\%%'")
112
+ expect(sql).not_to include("NOT LIKE '%test\\\\%%'")
113
+ when "PostGIS", "PostgreSQL"
114
+ expect(sql).to include("ILIKE '%test\\%%'")
115
+ expect(sql).not_to include("NOT ILIKE '%test\\%%'")
116
+ else
117
+ expect(sql).to include("LIKE '%test%%'")
118
+ expect(sql).not_to include("NOT LIKE '%test%%'")
119
+ end
120
+ end
121
+
122
+ it 'properly quotes values with wildcards for NOT LIKE predicates' do
123
+ ransack_hash = { name_not_cont: 'test%' }
124
+ sql = Person.ransack(ransack_hash).result.to_sql
125
+
126
+ # The % should be properly quoted in the SQL
127
+ case ActiveRecord::Base.connection.adapter_name
128
+ when "Mysql2"
129
+ expect(sql).to include("NOT LIKE '%test\\\\%%'")
130
+ when "PostGIS", "PostgreSQL"
131
+ expect(sql).to include("NOT ILIKE '%test\\%%'")
132
+ else
133
+ expect(sql).to include("NOT LIKE '%test%%'")
134
+ end
135
+ end
136
+ end
137
+
138
+ context 'with negative conditions on associations' do
139
+ it 'handles not_null predicate with true value correctly' do
140
+ ransack_hash = { comments_id_not_null: true }
141
+ sql = Person.ransack(ransack_hash).result.to_sql
142
+
143
+ # Should generate an IN query with IS NOT NULL condition
144
+ expect(sql).to include('IN (')
145
+ expect(sql).to include('IS NOT NULL')
146
+ expect(sql).not_to include('IS NULL')
147
+ end
148
+
149
+ it 'handles not_null predicate with false value correctly' do
150
+ ransack_hash = { comments_id_not_null: false }
151
+ sql = Person.ransack(ransack_hash).result.to_sql
152
+
153
+ # Should generate a NOT IN query with IS NULL condition
154
+ expect(sql).to include('NOT IN (')
155
+ expect(sql).to include('IS NULL')
156
+ expect(sql).not_to include('IS NOT NULL')
157
+ end
158
+
159
+ it 'handles not_cont predicate correctly' do
160
+ ransack_hash = { comments_body_not_cont: 'test' }
161
+ sql = Person.ransack(ransack_hash).result.to_sql
162
+
163
+ # Should generate a NOT IN query with LIKE condition (not NOT LIKE)
164
+ expect(sql).to include('NOT IN (')
165
+ expect(sql).to include("LIKE '%test%'")
166
+ expect(sql).not_to include("NOT LIKE '%test%'")
167
+ end
168
+ end
169
+
170
+ context 'with nested conditions' do
171
+ it 'correctly identifies non-nested conditions' do
172
+ condition = Condition.extract(
173
+ Context.for(Person), 'name_eq', 'Test'
174
+ )
175
+
176
+ # Create a mock parent table
177
+ parent_table = Person.arel_table
178
+
179
+ # Get the attribute name and make sure it starts with the table name
180
+ attribute = condition.attributes.first
181
+ expect(attribute.name).to eq('name')
182
+ expect(parent_table.name).to eq('people')
183
+
184
+ # The method should return false because 'name' doesn't start with 'people'
185
+ result = condition.send(:not_nested_condition, attribute, parent_table)
186
+ expect(result).to be false
187
+ end
188
+
189
+ it 'correctly identifies truly non-nested conditions when attribute name starts with table name' do
190
+ # Create a condition with an attribute that starts with the table name
191
+ condition = Condition.extract(
192
+ Context.for(Person), 'name_eq', 'Test'
193
+ )
194
+
195
+ # Modify the attribute name to start with the table name for testing purposes
196
+ attribute = condition.attributes.first
197
+ allow(attribute).to receive(:name).and_return('people_name')
198
+
199
+ # Create a parent table
200
+ parent_table = Person.arel_table
201
+
202
+ # Now the method should return true because 'people_name' starts with 'people'
203
+ result = condition.send(:not_nested_condition, attribute, parent_table)
204
+ expect(result).to be true
205
+ end
206
+
207
+ it 'correctly identifies nested conditions' do
208
+ condition = Condition.extract(
209
+ Context.for(Person), 'articles_title_eq', 'Test'
210
+ )
211
+
212
+ # Create a mock table alias
213
+ parent_table = Arel::Nodes::TableAlias.new(
214
+ Article.arel_table,
215
+ Article.arel_table
216
+ )
217
+
218
+ # Access the private method using send
219
+ result = condition.send(:not_nested_condition, condition.attributes.first, parent_table)
220
+
221
+ # Should return false for nested condition
222
+ expect(result).to be false
223
+ end
224
+ end
225
+
226
+ context 'with polymorphic associations and not_in predicate' do
227
+ before do
228
+ # Define test models for polymorphic associations
229
+ class ::TestTask < ActiveRecord::Base
230
+ self.table_name = 'tasks'
231
+ has_many :follows, primary_key: :uid, inverse_of: :followed, foreign_key: :followed_uid, class_name: 'TestFollow'
232
+ has_many :users, through: :follows, source: :follower, source_type: 'TestUser'
233
+
234
+ # Add ransackable_attributes method
235
+ def self.ransackable_attributes(auth_object = nil)
236
+ ["created_at", "id", "name", "uid", "updated_at"]
237
+ end
238
+
239
+ # Add ransackable_associations method
240
+ def self.ransackable_associations(auth_object = nil)
241
+ ["follows", "users"]
242
+ end
243
+ end
244
+
245
+ class ::TestFollow < ActiveRecord::Base
246
+ self.table_name = 'follows'
247
+ belongs_to :follower, polymorphic: true, foreign_key: :follower_uid, primary_key: :uid
248
+ belongs_to :followed, polymorphic: true, foreign_key: :followed_uid, primary_key: :uid
249
+
250
+ # Add ransackable_attributes method
251
+ def self.ransackable_attributes(auth_object = nil)
252
+ ["created_at", "followed_type", "followed_uid", "follower_type", "follower_uid", "id", "updated_at"]
253
+ end
254
+
255
+ # Add ransackable_associations method
256
+ def self.ransackable_associations(auth_object = nil)
257
+ ["followed", "follower"]
258
+ end
259
+ end
260
+
261
+ class ::TestUser < ActiveRecord::Base
262
+ self.table_name = 'users'
263
+ has_many :follows, primary_key: :uid, inverse_of: :follower, foreign_key: :follower_uid, class_name: 'TestFollow'
264
+ has_many :tasks, through: :follows, source: :followed, source_type: 'TestTask'
265
+
266
+ # Add ransackable_attributes method
267
+ def self.ransackable_attributes(auth_object = nil)
268
+ ["created_at", "id", "name", "uid", "updated_at"]
269
+ end
270
+
271
+ # Add ransackable_associations method
272
+ def self.ransackable_associations(auth_object = nil)
273
+ ["follows", "tasks"]
274
+ end
275
+ end
276
+
277
+ # Create tables if they don't exist
278
+ ActiveRecord::Base.connection.create_table(:tasks, force: true) do |t|
279
+ t.string :uid
280
+ t.string :name
281
+ t.timestamps null: false
282
+ end
283
+
284
+ ActiveRecord::Base.connection.create_table(:follows, force: true) do |t|
285
+ t.string :followed_uid, null: false
286
+ t.string :followed_type, null: false
287
+ t.string :follower_uid, null: false
288
+ t.string :follower_type, null: false
289
+ t.timestamps null: false
290
+ t.index [:followed_uid, :followed_type]
291
+ t.index [:follower_uid, :follower_type]
292
+ end
293
+
294
+ ActiveRecord::Base.connection.create_table(:users, force: true) do |t|
295
+ t.string :uid
296
+ t.string :name
297
+ t.timestamps null: false
298
+ end
299
+ end
300
+
301
+ after do
302
+ # Clean up test models and tables
303
+ Object.send(:remove_const, :TestTask)
304
+ Object.send(:remove_const, :TestFollow)
305
+ Object.send(:remove_const, :TestUser)
306
+
307
+ ActiveRecord::Base.connection.drop_table(:tasks, if_exists: true)
308
+ ActiveRecord::Base.connection.drop_table(:follows, if_exists: true)
309
+ ActiveRecord::Base.connection.drop_table(:users, if_exists: true)
310
+ end
311
+
312
+ it 'correctly handles not_in predicate with polymorphic associations' do
313
+ # Create the search
314
+ search = TestTask.ransack(users_uid_not_in: ['uid_example'])
315
+ sql = search.result.to_sql
316
+
317
+ # Verify the SQL contains the expected NOT IN clause
318
+ expect(sql).to include('NOT IN')
319
+ expect(sql).to include("follower_uid")
320
+ expect(sql).to include("followed_uid")
321
+ expect(sql).to include("'uid_example'")
322
+
323
+ # The SQL should include a reference to tasks.uid
324
+ expect(sql).to include("tasks")
325
+ expect(sql).to include("uid")
326
+
327
+ # The SQL should include a reference to follows table
328
+ expect(sql).to include("follows")
329
+ end
330
+ end
101
331
  end
102
332
  end
103
333
  end
@@ -3,7 +3,6 @@ require 'spec_helper'
3
3
  module Ransack
4
4
  module Nodes
5
5
  describe Grouping do
6
-
7
6
  before do
8
7
  @g = 1
9
8
  end
@@ -66,6 +65,7 @@ module Ransack
66
65
  }
67
66
  }
68
67
  end
68
+
69
69
  before { subject.conditions = conditions }
70
70
 
71
71
  it 'expect duplicates to be removed' do
@@ -98,6 +98,7 @@ module Ransack
98
98
  }
99
99
  }
100
100
  end
101
+
101
102
  before { subject.conditions = conditions }
102
103
 
103
104
  it 'expect them to be parsed as different and not as duplicates' do
@@ -105,7 +106,6 @@ module Ransack
105
106
  end
106
107
  end
107
108
  end
108
-
109
109
  end
110
110
  end
111
111
  end
@@ -71,6 +71,18 @@ module Ransack
71
71
  end
72
72
  end
73
73
 
74
+ [[], ["12"], ["101.5"]].each do |value|
75
+ context "with an array value (#{value.inspect})" do
76
+ let(:raw_value) { value }
77
+
78
+ it "should cast to integer as nil" do
79
+ result = subject.cast(:integer)
80
+
81
+ expect(result).to be nil
82
+ end
83
+ end
84
+ end
85
+
74
86
  ["12", "101.5"].each do |value|
75
87
  context "with a float value (#{value})" do
76
88
  let(:raw_value) { value }
@@ -109,7 +121,6 @@ module Ransack
109
121
  end
110
122
  end
111
123
  end
112
-
113
124
  end
114
125
  end
115
126
  end
@@ -5,7 +5,6 @@ module Ransack
5
5
  FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
6
6
 
7
7
  describe Predicate do
8
-
9
8
  before do
10
9
  @s = Search.new(Person)
11
10
  end
@@ -158,9 +157,10 @@ module Ransack
158
157
 
159
158
  describe 'cont' do
160
159
  it_has_behavior 'wildcard escaping', :name_cont,
161
- (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
160
+ (case ActiveRecord::Base.connection.adapter_name
161
+ when "PostGIS", "PostgreSQL"
162
162
  /"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
163
- elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
163
+ when "Mysql2"
164
164
  /`people`.`name` LIKE '%\\\\%.\\\\_\\\\\\\\%'/
165
165
  else
166
166
  /"people"."name" LIKE '%%._\\%'/
@@ -177,9 +177,10 @@ module Ransack
177
177
 
178
178
  describe 'not_cont' do
179
179
  it_has_behavior 'wildcard escaping', :name_not_cont,
180
- (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
180
+ (case ActiveRecord::Base.connection.adapter_name
181
+ when "PostGIS", "PostgreSQL"
181
182
  /"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
182
- elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
183
+ when "Mysql2"
183
184
  /`people`.`name` NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/
184
185
  else
185
186
  /"people"."name" NOT LIKE '%%._\\%'/
@@ -196,9 +197,12 @@ module Ransack
196
197
 
197
198
  describe 'i_cont' do
198
199
  it_has_behavior 'wildcard escaping', :name_i_cont,
199
- (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
200
+ (case ActiveRecord::Base.connection.adapter_name
201
+ when "PostGIS"
202
+ /LOWER\("people"."name"\) ILIKE '%\\%\\.\\_\\\\%'/
203
+ when "PostgreSQL"
200
204
  /"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
201
- elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
205
+ when "Mysql2"
202
206
  /LOWER\(`people`.`name`\) LIKE '%\\\\%.\\\\_\\\\\\\\%'/
203
207
  else
204
208
  /LOWER\("people"."name"\) LIKE '%%._\\%'/
@@ -215,9 +219,12 @@ module Ransack
215
219
 
216
220
  describe 'not_i_cont' do
217
221
  it_has_behavior 'wildcard escaping', :name_not_i_cont,
218
- (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
222
+ (case ActiveRecord::Base.connection.adapter_name
223
+ when "PostGIS"
224
+ /LOWER\("people"."name"\) NOT ILIKE '%\\%\\.\\_\\\\%'/
225
+ when "PostgreSQL"
219
226
  /"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
220
- elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
227
+ when "Mysql2"
221
228
  /LOWER\(`people`.`name`\) NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/
222
229
  else
223
230
  /LOWER\("people"."name"\) NOT LIKE '%%._\\%'/