ransack 1.7.0 → 2.4.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 (127) hide show
  1. checksums.yaml +5 -5
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/SECURITY.md +12 -0
  4. data/.github/workflows/test.yml +120 -0
  5. data/.gitignore +3 -0
  6. data/CHANGELOG.md +463 -27
  7. data/CONTRIBUTING.md +52 -22
  8. data/Gemfile +24 -24
  9. data/README.md +453 -126
  10. data/Rakefile +6 -25
  11. data/lib/polyamorous/activerecord_5.2_ruby_2/join_association.rb +24 -0
  12. data/lib/polyamorous/activerecord_5.2_ruby_2/join_dependency.rb +79 -0
  13. data/lib/polyamorous/activerecord_5.2_ruby_2/reflection.rb +11 -0
  14. data/lib/polyamorous/activerecord_6.0_ruby_2/join_association.rb +1 -0
  15. data/lib/polyamorous/activerecord_6.0_ruby_2/join_dependency.rb +80 -0
  16. data/lib/polyamorous/activerecord_6.0_ruby_2/reflection.rb +1 -0
  17. data/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +74 -0
  18. data/lib/polyamorous/activerecord_6.1_ruby_2/join_dependency.rb +93 -0
  19. data/lib/polyamorous/activerecord_6.1_ruby_2/reflection.rb +1 -0
  20. data/lib/polyamorous/join.rb +70 -0
  21. data/lib/polyamorous/polyamorous.rb +24 -0
  22. data/lib/polyamorous/swapping_reflection_class.rb +11 -0
  23. data/lib/polyamorous/tree_node.rb +7 -0
  24. data/lib/ransack/adapters/active_record/base.rb +27 -2
  25. data/lib/ransack/adapters/active_record/context.rb +213 -139
  26. data/lib/ransack/adapters/active_record/ransack/constants.rb +70 -55
  27. data/lib/ransack/adapters/active_record/ransack/context.rb +10 -18
  28. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +42 -32
  29. data/lib/ransack/adapters/active_record/ransack/translate.rb +1 -5
  30. data/lib/ransack/adapters/active_record/ransack/visitor.rb +23 -0
  31. data/lib/ransack/adapters/active_record.rb +11 -10
  32. data/lib/ransack/adapters.rb +45 -23
  33. data/lib/ransack/configuration.rb +107 -4
  34. data/lib/ransack/constants.rb +13 -26
  35. data/lib/ransack/context.rb +45 -33
  36. data/lib/ransack/helpers/form_builder.rb +21 -12
  37. data/lib/ransack/helpers/form_helper.rb +75 -70
  38. data/lib/ransack/locale/ar.yml +70 -0
  39. data/lib/ransack/locale/az.yml +70 -0
  40. data/lib/ransack/locale/bg.yml +70 -0
  41. data/lib/ransack/locale/ca.yml +70 -0
  42. data/lib/ransack/locale/da.yml +70 -0
  43. data/lib/ransack/locale/el.yml +70 -0
  44. data/lib/ransack/locale/es.yml +22 -22
  45. data/lib/ransack/locale/fa.yml +70 -0
  46. data/lib/ransack/locale/fi.yml +71 -0
  47. data/lib/ransack/locale/id.yml +70 -0
  48. data/lib/ransack/locale/it.yml +70 -0
  49. data/lib/ransack/locale/ja.yml +70 -0
  50. data/lib/ransack/locale/nl.yml +4 -4
  51. data/lib/ransack/locale/pt-BR.yml +70 -0
  52. data/lib/ransack/locale/ru.yml +70 -0
  53. data/lib/ransack/locale/sk.yml +70 -0
  54. data/lib/ransack/locale/tr.yml +70 -0
  55. data/lib/ransack/locale/{zh.yml → zh-CN.yml} +13 -13
  56. data/lib/ransack/locale/zh-TW.yml +70 -0
  57. data/lib/ransack/nodes/attribute.rb +5 -2
  58. data/lib/ransack/nodes/bindable.rb +18 -6
  59. data/lib/ransack/nodes/condition.rb +85 -28
  60. data/lib/ransack/nodes/grouping.rb +17 -11
  61. data/lib/ransack/nodes/sort.rb +9 -5
  62. data/lib/ransack/nodes/value.rb +74 -68
  63. data/lib/ransack/nodes.rb +1 -1
  64. data/lib/ransack/predicate.rb +17 -20
  65. data/lib/ransack/search.rb +17 -8
  66. data/lib/ransack/translate.rb +115 -115
  67. data/lib/ransack/version.rb +1 -1
  68. data/lib/ransack/visitor.rb +1 -12
  69. data/lib/ransack.rb +9 -9
  70. data/logo/ransack-h.png +0 -0
  71. data/logo/ransack-h.svg +34 -0
  72. data/logo/ransack-v.png +0 -0
  73. data/logo/ransack-v.svg +34 -0
  74. data/logo/ransack.png +0 -0
  75. data/logo/ransack.svg +21 -0
  76. data/ransack.gemspec +7 -24
  77. data/spec/console.rb +4 -0
  78. data/spec/helpers/polyamorous_helper.rb +19 -0
  79. data/spec/polyamorous/join_association_spec.rb +35 -0
  80. data/spec/polyamorous/join_dependency_spec.rb +97 -0
  81. data/spec/polyamorous/join_spec.rb +19 -0
  82. data/spec/ransack/adapters/active_record/base_spec.rb +370 -75
  83. data/spec/ransack/adapters/active_record/context_spec.rb +72 -34
  84. data/spec/ransack/configuration_spec.rb +97 -14
  85. data/spec/ransack/helpers/form_builder_spec.rb +2 -11
  86. data/spec/ransack/helpers/form_helper_spec.rb +481 -113
  87. data/spec/ransack/nodes/condition_spec.rb +24 -0
  88. data/spec/ransack/nodes/grouping_spec.rb +56 -0
  89. data/spec/ransack/predicate_spec.rb +79 -5
  90. data/spec/ransack/search_spec.rb +207 -81
  91. data/spec/spec_helper.rb +8 -0
  92. data/spec/support/schema.rb +100 -42
  93. metadata +57 -184
  94. data/.travis.yml +0 -69
  95. data/lib/ransack/adapters/active_record/3.0/compat.rb +0 -179
  96. data/lib/ransack/adapters/active_record/3.0/context.rb +0 -201
  97. data/lib/ransack/adapters/active_record/3.1/context.rb +0 -215
  98. data/lib/ransack/adapters/active_record/3.2/context.rb +0 -44
  99. data/lib/ransack/adapters/active_record/compat.rb +0 -14
  100. data/lib/ransack/adapters/mongoid/3.2/.gitkeep +0 -0
  101. data/lib/ransack/adapters/mongoid/attributes/attribute.rb +0 -37
  102. data/lib/ransack/adapters/mongoid/attributes/order_predications.rb +0 -17
  103. data/lib/ransack/adapters/mongoid/attributes/predications.rb +0 -141
  104. data/lib/ransack/adapters/mongoid/base.rb +0 -130
  105. data/lib/ransack/adapters/mongoid/context.rb +0 -208
  106. data/lib/ransack/adapters/mongoid/inquiry_hash.rb +0 -23
  107. data/lib/ransack/adapters/mongoid/ransack/constants.rb +0 -88
  108. data/lib/ransack/adapters/mongoid/ransack/context.rb +0 -60
  109. data/lib/ransack/adapters/mongoid/ransack/nodes/condition.rb +0 -27
  110. data/lib/ransack/adapters/mongoid/ransack/translate.rb +0 -13
  111. data/lib/ransack/adapters/mongoid/ransack/visitor.rb +0 -24
  112. data/lib/ransack/adapters/mongoid/table.rb +0 -35
  113. data/lib/ransack/adapters/mongoid.rb +0 -13
  114. data/spec/mongoid/adapters/mongoid/base_spec.rb +0 -276
  115. data/spec/mongoid/adapters/mongoid/context_spec.rb +0 -56
  116. data/spec/mongoid/configuration_spec.rb +0 -102
  117. data/spec/mongoid/dependencies_spec.rb +0 -8
  118. data/spec/mongoid/helpers/ransack_helper.rb +0 -11
  119. data/spec/mongoid/nodes/condition_spec.rb +0 -34
  120. data/spec/mongoid/nodes/grouping_spec.rb +0 -13
  121. data/spec/mongoid/predicate_spec.rb +0 -155
  122. data/spec/mongoid/search_spec.rb +0 -446
  123. data/spec/mongoid/support/mongoid.yml +0 -6
  124. data/spec/mongoid/support/schema.rb +0 -128
  125. data/spec/mongoid/translate_spec.rb +0 -14
  126. data/spec/mongoid_spec_helper.rb +0 -59
  127. data/spec/ransack/dependencies_spec.rb +0 -12
@@ -1,42 +1,59 @@
1
1
  require 'ransack/context'
2
- require 'ransack/adapters/active_record/compat'
3
- require 'polyamorous'
2
+ require 'polyamorous/polyamorous'
4
3
 
5
4
  module Ransack
6
5
  module Adapters
7
6
  module ActiveRecord
8
7
  class Context < ::Ransack::Context
9
8
 
10
- # Because the AR::Associations namespace is insane
11
- JoinDependency = ::ActiveRecord::Associations::JoinDependency
12
- JoinPart = JoinDependency::JoinPart
13
-
14
- def initialize(object, options = {})
15
- super
16
- @arel_visitor = @engine.connection.visitor
17
- end
18
-
19
9
  def relation_for(object)
20
10
  object.all
21
11
  end
22
12
 
23
13
  def type_for(attr)
24
14
  return nil unless attr && attr.valid?
25
- name = attr.arel_attribute.name.to_s
26
- table = attr.arel_attribute.relation.table_name
27
- connection = attr.klass.connection
28
- unless connection.table_exists?(table)
29
- raise "No table named #{table} exists"
15
+ name = attr.arel_attribute.name.to_s
16
+ table = attr.arel_attribute.relation.table_name
17
+ schema_cache = self.klass.connection.schema_cache
18
+ unless schema_cache.send(:data_source_exists?, table)
19
+ raise "No table named #{table} exists."
30
20
  end
31
- connection.schema_cache.columns_hash(table)[name].type
21
+ attr.klass.columns.find { |column| column.name == name }.type
32
22
  end
33
23
 
34
24
  def evaluate(search, opts = {})
35
25
  viz = Visitor.new
36
26
  relation = @object.where(viz.accept(search.base))
27
+
37
28
  if search.sorts.any?
38
- relation = relation.except(:order).reorder(viz.accept(search.sorts))
29
+ relation = relation.except(:order)
30
+ # Rather than applying all of the search's sorts in one fell swoop,
31
+ # as the original implementation does, we apply one at a time.
32
+ #
33
+ # If the sort (returned by the Visitor above) is a symbol, we know
34
+ # that it represents a scope on the model and we can apply that
35
+ # scope.
36
+ #
37
+ # Otherwise, we fall back to the applying the sort with the "order"
38
+ # method as the original implementation did. Actually the original
39
+ # implementation used "reorder," which was overkill since we already
40
+ # have a clean slate after "relation.except(:order)" above.
41
+ viz.accept(search.sorts).each do |scope_or_sort|
42
+ if scope_or_sort.is_a?(Symbol)
43
+ relation = relation.send(scope_or_sort)
44
+ else
45
+ case Ransack.options[:postgres_fields_sort_option]
46
+ when :nulls_first
47
+ scope_or_sort = scope_or_sort.direction == :asc ? "#{scope_or_sort.to_sql} NULLS FIRST" : "#{scope_or_sort.to_sql} NULLS LAST"
48
+ when :nulls_last
49
+ scope_or_sort = scope_or_sort.direction == :asc ? "#{scope_or_sort.to_sql} NULLS LAST" : "#{scope_or_sort.to_sql} NULLS FIRST"
50
+ end
51
+
52
+ relation = relation.order(scope_or_sort)
53
+ end
54
+ end
39
55
  end
56
+
40
57
  opts[:distinct] ? relation.distinct : relation
41
58
  end
42
59
 
@@ -44,7 +61,7 @@ module Ransack
44
61
  exists = false
45
62
  if ransackable_attribute?(str, klass)
46
63
  exists = true
47
- elsif (segments = str.split(/_/)).size > 1
64
+ elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
48
65
  remainder = []
49
66
  found_assoc = nil
50
67
  while !found_assoc && remainder.unshift(segments.pop) &&
@@ -79,61 +96,119 @@ module Ransack
79
96
  end
80
97
  end
81
98
 
82
- if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
99
+ # All dependent Arel::Join nodes used in the search query.
100
+ #
101
+ # This could otherwise be done as `@object.arel.join_sources`, except
102
+ # that ActiveRecord's build_joins sets up its own JoinDependency.
103
+ # This extracts what we need to access the joins using our existing
104
+ # JoinDependency to track table aliases.
105
+ #
106
+ def join_sources
107
+ base, joins = begin
108
+ alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, @object.table.name, [])
109
+ constraints = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_1_ALPHA)
110
+ @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)
111
+ elsif ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
112
+ @join_dependency.join_constraints(@object.joins_values, alias_tracker)
113
+ else
114
+ @join_dependency.join_constraints(@object.joins_values, @join_type, alias_tracker)
115
+ end
83
116
 
84
- def join_associations
85
- raise NotImplementedError,
86
- "ActiveRecord 4.1 and later does not use join_associations. Use join_sources."
117
+ [
118
+ Arel::SelectManager.new(@object.table),
119
+ constraints
120
+ ]
87
121
  end
88
-
89
- # All dependent Arel::Join nodes used in the search query
90
- #
91
- # This could otherwise be done as `@object.arel.join_sources`, except
92
- # that ActiveRecord's build_joins sets up its own JoinDependency.
93
- # This extracts what we need to access the joins using our existing
94
- # JoinDependency to track table aliases.
95
- #
96
- def join_sources
97
- base =
98
- if ::ActiveRecord::VERSION::MAJOR >= 5
99
- Arel::SelectManager.new(@object.table)
100
- else
101
- Arel::SelectManager.new(@object.engine, @object.table)
102
- end
103
- joins = @join_dependency.join_constraints(@object.joins_values)
104
- joins.each do |aliased_join|
105
- base.from(aliased_join)
106
- end
107
- base.join_sources
122
+ joins.each do |aliased_join|
123
+ base.from(aliased_join)
108
124
  end
125
+ base.join_sources
126
+ end
109
127
 
110
- else
128
+ def alias_tracker
129
+ @join_dependency.send(:alias_tracker)
130
+ end
111
131
 
112
- # All dependent JoinAssociation items used in the search query
113
- #
114
- # Deprecated: this goes away in ActiveRecord 4.1. Use join_sources.
115
- #
116
- def join_associations
117
- @join_dependency.join_associations
118
- end
132
+ def lock_association(association)
133
+ @lock_associations << association
134
+ end
119
135
 
120
- def join_sources
121
- base = Arel::SelectManager.new(@object.engine, @object.table)
122
- joins = @object.joins_values
123
- joins.each do |assoc|
124
- assoc.join_to(base)
125
- end
126
- base.join_sources
127
- end
136
+ def remove_association(association)
137
+ return if @lock_associations.include?(association)
138
+ @join_dependency.instance_variable_get(:@join_root).children.delete_if { |stashed|
139
+ stashed.eql?(association)
140
+ }
141
+ @object.joins_values.delete_if { |jd|
142
+ jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
143
+ }
144
+ end
128
145
 
146
+ # Build an Arel subquery that selects keys for the top query,
147
+ # drawn from the first join association's foreign_key.
148
+ #
149
+ # Example: for an Article that has_and_belongs_to_many Tags
150
+ #
151
+ # context = Article.search.context
152
+ # attribute = Attribute.new(context, "tags_name").tap do |a|
153
+ # context.bind(a, a.name)
154
+ # end
155
+ # context.build_correlated_subquery(attribute.parent).to_sql
156
+ #
157
+ # # SELECT "articles_tags"."article_id" FROM "articles_tags"
158
+ # # INNER JOIN "tags" ON "tags"."id" = "articles_tags"."tag_id"
159
+ # # WHERE "articles_tags"."article_id" = "articles"."id"
160
+ #
161
+ # The WHERE condition on this query makes it invalid by itself,
162
+ # because it is correlated to the primary key on the outer query.
163
+ #
164
+ def build_correlated_subquery(association)
165
+ join_constraints = extract_joins(association)
166
+ join_root = join_constraints.shift
167
+ correlated_key = extract_correlated_key(join_root)
168
+ subquery = Arel::SelectManager.new(association.base_klass)
169
+ subquery.from(join_root.left)
170
+ subquery.project(correlated_key)
171
+ join_constraints.each do |j|
172
+ subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
173
+ end
174
+ subquery.where(correlated_key.eq(primary_key))
129
175
  end
130
176
 
131
- def alias_tracker
132
- @join_dependency.alias_tracker
177
+ def primary_key
178
+ @object.table[@object.primary_key]
133
179
  end
134
180
 
135
181
  private
136
182
 
183
+ def extract_correlated_key(join_root)
184
+ case join_root
185
+ when Arel::Nodes::OuterJoin
186
+ # one of join_root.right/join_root.left is expected to be Arel::Nodes::On
187
+ if join_root.right.is_a?(Arel::Nodes::On)
188
+ extract_correlated_key(join_root.right.expr)
189
+ elsif join_root.left.is_a?(Arel::Nodes::On)
190
+ extract_correlated_key(join_root.left.expr)
191
+ else
192
+ raise 'Ransack encountered an unexpected arel structure'
193
+ end
194
+ when Arel::Nodes::Equality
195
+ pk = primary_key
196
+ if join_root.left == pk
197
+ join_root.right
198
+ elsif join_root.right == pk
199
+ join_root.left
200
+ else
201
+ nil
202
+ end
203
+ when Arel::Nodes::And
204
+ extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
205
+ else
206
+ # eg parent was Arel::Nodes::And and the evaluated side was one of
207
+ # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
208
+ nil
209
+ end
210
+ end
211
+
137
212
  def get_parent_and_attribute_name(str, parent = @base)
138
213
  attr_name = nil
139
214
 
@@ -168,7 +243,7 @@ module Ransack
168
243
  end
169
244
 
170
245
  def join_dependency(relation)
171
- if relation.respond_to?(:join_dependency) # Squeel will enable this
246
+ if relation.respond_to?(:join_dependency) # Polyamorous enables this
172
247
  relation.join_dependency
173
248
  else
174
249
  build_joins(relation)
@@ -178,7 +253,9 @@ module Ransack
178
253
  # Checkout active_record/relation/query_methods.rb +build_joins+ for
179
254
  # reference. Lots of duplicated code maybe we can avoid it
180
255
  def build_joins(relation)
181
- buckets = relation.joins_values.group_by do |join|
256
+ buckets = relation.joins_values + relation.left_outer_joins_values
257
+
258
+ buckets = buckets.group_by do |join|
182
259
  case join
183
260
  when String
184
261
  :string_join
@@ -196,102 +273,99 @@ module Ransack
196
273
  association_joins = buckets[:association_join]
197
274
  stashed_association_joins = buckets[:stashed_join]
198
275
  join_nodes = buckets[:join_node].uniq
199
- string_joins = buckets[:string_join].map(&:strip).uniq
200
-
201
- join_list =
202
- if ::ActiveRecord::VERSION::MAJOR >= 5
203
- join_nodes +
204
- convert_join_strings_to_ast(relation.table, string_joins)
205
- else
206
- relation.send :custom_join_ast,
207
- relation.table.from(relation.table), string_joins
208
- end
276
+ string_joins = buckets[:string_join].map(&:strip)
277
+ string_joins.uniq!
209
278
 
210
- join_dependency = JoinDependency.new(
211
- relation.klass, association_joins, join_list
212
- )
279
+ join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)
213
280
 
214
- join_nodes.each do |join|
215
- join_dependency.alias_tracker.aliases[join.left.name.downcase] = 1
216
- end
217
-
218
- if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
219
- join_dependency
281
+ alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, relation.table.name, join_list)
282
+ join_dependency = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
283
+ Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
220
284
  else
221
- join_dependency.graft(*stashed_association_joins)
285
+ Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins)
286
+ end
287
+ join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
288
+ join_nodes.each do |join|
289
+ join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
222
290
  end
291
+ join_dependency
223
292
  end
224
293
 
225
294
  def convert_join_strings_to_ast(table, joins)
295
+ joins.map! { |join| table.create_string_join(Arel.sql(join)) unless join.blank? }
296
+ joins.compact!
226
297
  joins
227
- .flatten
228
- .reject(&:blank?)
229
- .map { |join| table.create_string_join(Arel.sql(join)) }
230
298
  end
231
299
 
232
- if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
233
-
234
- def build_or_find_association(name, parent = @base, klass = nil)
235
- found_association = @join_dependency.join_root.children
236
- .detect do |assoc|
237
- assoc.reflection.name == name &&
238
- (@associations_pot.nil? || @associations_pot[assoc] == parent) &&
239
- (!klass || assoc.reflection.klass == klass)
240
- end
241
-
242
- unless found_association
243
- jd = JoinDependency.new(
244
- parent.base_klass,
245
- Polyamorous::Join.new(name, @join_type, klass),
246
- []
247
- )
248
- found_association = jd.join_root.children.last
249
- associations found_association, parent
250
-
251
- # TODO maybe we dont need to push associations here, we could loop
252
- # through the @associations_pot instead
253
- @join_dependency.join_root.children.push found_association
254
-
255
- # Builds the arel nodes properly for this association
256
- @join_dependency.send(
257
- :construct_tables!, jd.join_root, found_association
258
- )
300
+ def build_or_find_association(name, parent = @base, klass = nil)
301
+ find_association(name, parent, klass) or build_association(name, parent, klass)
302
+ end
259
303
 
260
- # Leverage the stashed association functionality in AR
261
- @object = @object.joins(jd)
262
- end
263
- found_association
304
+ def find_association(name, parent = @base, klass = nil)
305
+ @join_dependency.instance_variable_get(:@join_root).children.detect do |assoc|
306
+ assoc.reflection.name == name && assoc.table &&
307
+ (@associations_pot.empty? || @associations_pot[assoc] == parent || !@associations_pot.key?(assoc)) &&
308
+ (!klass || assoc.reflection.klass == klass)
264
309
  end
310
+ end
265
311
 
266
- def associations(assoc, parent)
267
- @associations_pot ||= {}
268
- @associations_pot[assoc] = parent
312
+ def build_association(name, parent = @base, klass = nil)
313
+ if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
314
+ jd = Polyamorous::JoinDependency.new(
315
+ parent.base_klass,
316
+ parent.table,
317
+ Polyamorous::Join.new(name, @join_type, klass),
318
+ @join_type
319
+ )
320
+ found_association = jd.instance_variable_get(:@join_root).children.last
321
+ else
322
+ jd = Polyamorous::JoinDependency.new(
323
+ parent.base_klass,
324
+ parent.table,
325
+ Polyamorous::Join.new(name, @join_type, klass)
326
+ )
327
+ found_association = jd.instance_variable_get(:@join_root).children.last
269
328
  end
270
329
 
271
- else
330
+ @associations_pot[found_association] = parent
272
331
 
273
- def build_or_find_association(name, parent = @base, klass = nil)
274
- found_association = @join_dependency.join_associations
275
- .detect do |assoc|
276
- assoc.reflection.name == name &&
277
- assoc.parent == parent &&
278
- (!klass || assoc.reflection.klass == klass)
279
- end
280
- unless found_association
281
- @join_dependency.send(
282
- :build,
283
- Polyamorous::Join.new(name, @join_type, klass),
284
- parent
285
- )
286
- found_association = @join_dependency.join_associations.last
287
- # Leverage the stashed association functionality in AR
288
- @object = @object.joins(found_association)
289
- end
290
- found_association
332
+ # TODO maybe we dont need to push associations here, we could loop
333
+ # through the @associations_pot instead
334
+ @join_dependency.instance_variable_get(:@join_root).children.push found_association
335
+
336
+ # Builds the arel nodes properly for this association
337
+ if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_1_ALPHA)
338
+ @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)
339
+ else
340
+ @join_dependency.send(:construct_tables!, jd.instance_variable_get(:@join_root))
291
341
  end
292
342
 
343
+ # Leverage the stashed association functionality in AR
344
+ @object = @object.joins(jd)
345
+ found_association
293
346
  end
294
347
 
348
+ def extract_joins(association)
349
+ parent = @join_dependency.instance_variable_get(:@join_root)
350
+ reflection = association.reflection
351
+ join_constraints = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_1_ALPHA)
352
+ association.join_constraints_with_tables(
353
+ parent.table,
354
+ parent.base_klass,
355
+ Arel::Nodes::OuterJoin,
356
+ @join_dependency.instance_variable_get(:@alias_tracker),
357
+ @tables_pot[association]
358
+ )
359
+ else
360
+ association.join_constraints(
361
+ parent.table,
362
+ parent.base_klass,
363
+ Arel::Nodes::OuterJoin,
364
+ @join_dependency.instance_variable_get(:@alias_tracker)
365
+ )
366
+ end
367
+ join_constraints.to_a.flatten
368
+ end
295
369
  end
296
370
  end
297
371
  end
@@ -4,97 +4,109 @@ module Ransack
4
4
 
5
5
  DERIVED_PREDICATES = [
6
6
  [CONT, {
7
- :arel_predicate => 'matches'.freeze,
8
- :formatter => proc { |v| "%#{escape_wildcards(v)}%" }
7
+ arel_predicate: 'matches'.freeze,
8
+ formatter: proc { |v| "%#{escape_wildcards(v)}%" }
9
9
  }
10
10
  ],
11
11
  ['not_cont'.freeze, {
12
- :arel_predicate => 'does_not_match'.freeze,
13
- :formatter => proc { |v| "%#{escape_wildcards(v)}%" }
12
+ arel_predicate: 'does_not_match'.freeze,
13
+ formatter: proc { |v| "%#{escape_wildcards(v)}%" }
14
+ }
15
+ ],
16
+ ['i_cont'.freeze, {
17
+ arel_predicate: 'matches'.freeze,
18
+ formatter: proc { |v| "%#{escape_wildcards(v.downcase)}%" },
19
+ case_insensitive: true
20
+ }
21
+ ],
22
+ ['not_i_cont'.freeze, {
23
+ arel_predicate: 'does_not_match'.freeze,
24
+ formatter: proc { |v| "%#{escape_wildcards(v.downcase)}%" },
25
+ case_insensitive: true
14
26
  }
15
27
  ],
16
28
  ['start'.freeze, {
17
- :arel_predicate => 'matches'.freeze,
18
- :formatter => proc { |v| "#{escape_wildcards(v)}%" }
29
+ arel_predicate: 'matches'.freeze,
30
+ formatter: proc { |v| "#{escape_wildcards(v)}%" }
19
31
  }
20
32
  ],
21
33
  ['not_start'.freeze, {
22
- :arel_predicate => 'does_not_match'.freeze,
23
- :formatter => proc { |v| "#{escape_wildcards(v)}%" }
34
+ arel_predicate: 'does_not_match'.freeze,
35
+ formatter: proc { |v| "#{escape_wildcards(v)}%" }
24
36
  }
25
37
  ],
26
38
  ['end'.freeze, {
27
- :arel_predicate => 'matches'.freeze,
28
- :formatter => proc { |v| "%#{escape_wildcards(v)}" }
39
+ arel_predicate: 'matches'.freeze,
40
+ formatter: proc { |v| "%#{escape_wildcards(v)}" }
29
41
  }
30
42
  ],
31
43
  ['not_end'.freeze, {
32
- :arel_predicate => 'does_not_match'.freeze,
33
- :formatter => proc { |v| "%#{escape_wildcards(v)}" }
44
+ arel_predicate: 'does_not_match'.freeze,
45
+ formatter: proc { |v| "%#{escape_wildcards(v)}" }
34
46
  }
35
47
  ],
36
48
  ['true'.freeze, {
37
- :arel_predicate => proc { |v| v ? EQ : NOT_EQ },
38
- :compounds => false,
39
- :type => :boolean,
40
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
41
- :formatter => proc { |v| true }
49
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
50
+ compounds: false,
51
+ type: :boolean,
52
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
53
+ formatter: proc { |v| true }
42
54
  }
43
55
  ],
44
56
  ['not_true'.freeze, {
45
- :arel_predicate => proc { |v| v ? NOT_EQ : EQ },
46
- :compounds => false,
47
- :type => :boolean,
48
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
49
- :formatter => proc { |v| true }
57
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
58
+ compounds: false,
59
+ type: :boolean,
60
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
61
+ formatter: proc { |v| true }
50
62
  }
51
63
  ],
52
64
  ['false'.freeze, {
53
- :arel_predicate => proc { |v| v ? EQ : NOT_EQ },
54
- :compounds => false,
55
- :type => :boolean,
56
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
57
- :formatter => proc { |v| false }
65
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
66
+ compounds: false,
67
+ type: :boolean,
68
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
69
+ formatter: proc { |v| false }
58
70
  }
59
71
  ],
60
72
  ['not_false'.freeze, {
61
- :arel_predicate => proc { |v| v ? NOT_EQ : EQ },
62
- :compounds => false,
63
- :type => :boolean,
64
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
65
- :formatter => proc { |v| false }
73
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
74
+ compounds: false,
75
+ type: :boolean,
76
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
77
+ formatter: proc { |v| false }
66
78
  }
67
79
  ],
68
80
  ['present'.freeze, {
69
- :arel_predicate => proc { |v| v ? NOT_EQ_ALL : EQ_ANY },
70
- :compounds => false,
71
- :type => :boolean,
72
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
73
- :formatter => proc { |v| [nil, EMPTY] }
81
+ arel_predicate: proc { |v| v ? NOT_EQ_ALL : EQ_ANY },
82
+ compounds: false,
83
+ type: :boolean,
84
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
85
+ formatter: proc { |v| [nil, ''.freeze].freeze }
74
86
  }
75
87
  ],
76
88
  ['blank'.freeze, {
77
- :arel_predicate => proc { |v| v ? EQ_ANY : NOT_EQ_ALL },
78
- :compounds => false,
79
- :type => :boolean,
80
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
81
- :formatter => proc { |v| [nil, EMPTY] }
89
+ arel_predicate: proc { |v| v ? EQ_ANY : NOT_EQ_ALL },
90
+ compounds: false,
91
+ type: :boolean,
92
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
93
+ formatter: proc { |v| [nil, ''.freeze].freeze }
82
94
  }
83
95
  ],
84
96
  ['null'.freeze, {
85
- :arel_predicate => proc { |v| v ? EQ : NOT_EQ },
86
- :compounds => false,
87
- :type => :boolean,
88
- :validator => proc { |v| BOOLEAN_VALUES.include?(v)},
89
- :formatter => proc { |v| nil }
97
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
98
+ compounds: false,
99
+ type: :boolean,
100
+ validator: proc { |v| BOOLEAN_VALUES.include?(v)},
101
+ formatter: proc { |v| nil }
90
102
  }
91
103
  ],
92
104
  ['not_null'.freeze, {
93
- :arel_predicate => proc { |v| v ? NOT_EQ : EQ },
94
- :compounds => false,
95
- :type => :boolean,
96
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
97
- :formatter => proc { |v| nil } }
105
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
106
+ compounds: false,
107
+ type: :boolean,
108
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
109
+ formatter: proc { |v| nil } }
98
110
  ]
99
111
  ].freeze
100
112
 
@@ -102,9 +114,12 @@ module Ransack
102
114
  # replace % \ to \% \\
103
115
  def escape_wildcards(unescaped)
104
116
  case ActiveRecord::Base.connection.adapter_name
105
- when "Mysql2".freeze, "PostgreSQL".freeze
106
- # Necessary for PostgreSQL and MySQL
107
- unescaped.to_s.gsub(/([\\|\%|.])/, '\\\\\\1')
117
+ when "Mysql2".freeze
118
+ # Necessary for MySQL
119
+ unescaped.to_s.gsub(/([\\%_])/, '\\\\\\1')
120
+ when "PostgreSQL".freeze
121
+ # Necessary for PostgreSQL
122
+ unescaped.to_s.gsub(/([\\%_.])/, '\\\\\\1')
108
123
  else
109
124
  unescaped
110
125
  end
@@ -27,23 +27,19 @@ module Ransack
27
27
  @join_dependency = join_dependency(@object)
28
28
  @join_type = options[:join_type] || Polyamorous::OuterJoin
29
29
  @search_key = options[:search_key] || Ransack.options[:search_key]
30
+ @associations_pot = {}
31
+ @tables_pot = {}
32
+ @lock_associations = []
30
33
 
31
- if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
32
- @base = @join_dependency.join_root
33
- @engine = @base.base_klass.arel_engine
34
- else
35
- @base = @join_dependency.join_base
36
- @engine = @base.arel_engine
37
- end
34
+ @base = @join_dependency.instance_variable_get(:@join_root)
35
+ end
38
36
 
39
- @default_table = Arel::Table.new(
40
- @base.table_name, as: @base.aliased_table_name, type_caster: self
41
- )
42
- @bind_pairs = Hash.new do |hash, key|
37
+ def bind_pair_for(key)
38
+ @bind_pairs ||= {}
39
+
40
+ @bind_pairs[key] ||= begin
43
41
  parent, attr_name = get_parent_and_attribute_name(key.to_s)
44
- if parent && attr_name
45
- hash[key] = [parent, attr_name]
46
- end
42
+ [parent, attr_name] if parent && attr_name
47
43
  end
48
44
  end
49
45
 
@@ -52,10 +48,6 @@ module Ransack
52
48
  obj
53
49
  elsif obj.respond_to? :klass
54
50
  obj.klass
55
- elsif obj.respond_to? :active_record # Rails 3
56
- obj.active_record
57
- elsif obj.respond_to? :base_klass # Rails 4
58
- obj.base_klass
59
51
  else
60
52
  raise ArgumentError, "Don't know how to klassify #{obj.inspect}"
61
53
  end