ransack 5.0.2 → 6.0.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -27
  3. data/lib/ransack/active_record/base.rb +154 -0
  4. data/lib/ransack/active_record/context.rb +504 -0
  5. data/lib/ransack/active_record/dialect.rb +83 -0
  6. data/lib/ransack/active_record/join.rb +75 -0
  7. data/lib/ransack/active_record/join_association.rb +104 -0
  8. data/lib/ransack/active_record/join_dependency.rb +123 -0
  9. data/lib/ransack/active_record/reflection.rb +18 -0
  10. data/lib/ransack/active_record/tree_node.rb +13 -0
  11. data/lib/ransack/active_record.rb +44 -9
  12. data/lib/ransack/configuration.rb +49 -1
  13. data/lib/ransack/constants.rb +52 -0
  14. data/lib/ransack/context.rb +159 -35
  15. data/lib/ransack/helpers/form_builder.rb +30 -25
  16. data/lib/ransack/nodes/bindable.rb +0 -1
  17. data/lib/ransack/nodes/condition.rb +215 -73
  18. data/lib/ransack/nodes/grouping.rb +33 -11
  19. data/lib/ransack/nodes/sort.rb +1 -1
  20. data/lib/ransack/nodes/value.rb +8 -1
  21. data/lib/ransack/predicate.rb +8 -1
  22. data/lib/ransack/search.rb +52 -9
  23. data/lib/ransack/version.rb +1 -1
  24. data/lib/ransack.rb +4 -1
  25. metadata +11 -13
  26. data/lib/polyamorous/activerecord/join_association.rb +0 -70
  27. data/lib/polyamorous/activerecord/join_association_7_2.rb +0 -55
  28. data/lib/polyamorous/activerecord/join_dependency.rb +0 -102
  29. data/lib/polyamorous/activerecord/reflection.rb +0 -11
  30. data/lib/polyamorous/join.rb +0 -70
  31. data/lib/polyamorous/polyamorous.rb +0 -27
  32. data/lib/polyamorous/swapping_reflection_class.rb +0 -11
  33. data/lib/polyamorous/tree_node.rb +0 -7
  34. data/lib/ransack/adapters/active_record/base.rb +0 -152
  35. data/lib/ransack/adapters/active_record/context.rb +0 -379
@@ -0,0 +1,504 @@
1
+ require 'ransack/context'
2
+
3
+ module Ransack
4
+ module ActiveRecord
5
+ # The Active Record implementation of Ransack::Context: resolves attribute
6
+ # and association names against a model, builds the joins a search needs
7
+ # on top of whatever joins the relation already has, and turns the search
8
+ # tree into a relation.
9
+ class Context < ::Ransack::Context
10
+
11
+ def relation_for(object)
12
+ object.all
13
+ end
14
+
15
+ def dialect
16
+ @dialect ||= Dialect.for(@klass)
17
+ end
18
+
19
+ # The type a value for this attribute is cast to. Read through the
20
+ # Attributes API rather than the schema column, so that
21
+ # `attribute :starts_at, :datetime` on a date column is honoured (#1028);
22
+ # a column with no attribute override reports its column type.
23
+ def type_for(attr)
24
+ return nil unless attr && attr.valid?
25
+ relation = attr.arel_attribute.relation
26
+ name = attr.arel_attribute.name.to_s
27
+ table = relation.respond_to?(:table_name) ? relation.table_name : relation.name
28
+ schema_cache = self.klass.connection_pool.schema_cache
29
+ unless schema_cache.send(:data_source_exists?, table)
30
+ raise "No table named #{table} exists."
31
+ end
32
+ return nil unless attr.klass.attribute_types.key?(name)
33
+
34
+ attr.klass.type_for_attribute(name).type
35
+ end
36
+
37
+ def evaluate(search, opts = {})
38
+ viz = Visitor.new
39
+ relation = @object.where(viz.accept(search.base))
40
+
41
+ if search.sorts.any?
42
+ relation = relation.except(:order)
43
+ # Rather than applying all of the search's sorts in one fell swoop,
44
+ # as the original implementation does, we apply one at a time.
45
+ #
46
+ # If the sort (returned by the Visitor above) is a symbol, we know
47
+ # that it represents a scope on the model and we can apply that
48
+ # scope.
49
+ #
50
+ # Otherwise, we fall back to the applying the sort with the "order"
51
+ # method as the original implementation did. Actually the original
52
+ # implementation used "reorder," which was overkill since we already
53
+ # have a clean slate after "relation.except(:order)" above.
54
+ viz.accept(search.sorts).each do |scope_or_sort|
55
+ if scope_or_sort.is_a?(Symbol)
56
+ relation = relation.send(scope_or_sort)
57
+ else
58
+ case Ransack.options[:fields_sort_option]
59
+ when :nulls_first
60
+ scope_or_sort = scope_or_sort.direction == :asc ? scope_or_sort.nulls_first : scope_or_sort.nulls_last
61
+ when :nulls_last
62
+ scope_or_sort = scope_or_sort.direction == :asc ? scope_or_sort.nulls_last : scope_or_sort.nulls_first
63
+ when :nulls_always_first
64
+ scope_or_sort = scope_or_sort.nulls_first
65
+ when :nulls_always_last
66
+ scope_or_sort = scope_or_sort.nulls_last
67
+ end
68
+
69
+ relation = relation.order(scope_or_sort)
70
+ end
71
+ end
72
+ end
73
+
74
+ opts[:distinct] ? relation.distinct : relation
75
+ end
76
+
77
+ def attribute_method?(str, klass = @klass)
78
+ return false unless key_within_depth_limit?(str)
79
+
80
+ exists = false
81
+ if ransackable_attribute?(str, klass) ||
82
+ ransortable_attribute?(str, klass)
83
+ exists = true
84
+ elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
85
+ remainder = []
86
+ found_assoc = nil
87
+ while !found_assoc && remainder.unshift(segments.pop) &&
88
+ segments.size > 0 do
89
+ assoc, poly_class = unpolymorphize_association(
90
+ segments.join(Constants::UNDERSCORE)
91
+ )
92
+ if found_assoc = traversable_association(assoc, poly_class, klass)
93
+ exists = attribute_method?(
94
+ remainder.join(Constants::UNDERSCORE),
95
+ poly_class || found_assoc.klass
96
+ )
97
+ end
98
+ end
99
+ end
100
+ exists
101
+ end
102
+
103
+ def table_for(parent)
104
+ parent.table
105
+ end
106
+
107
+ def klassify(obj)
108
+ if Class === obj && ::ActiveRecord::Base > obj
109
+ obj
110
+ elsif obj.respond_to? :klass
111
+ obj.klass
112
+ elsif obj.respond_to? :base_klass
113
+ obj.base_klass
114
+ else
115
+ raise ArgumentError, "Don't know how to klassify #{obj}"
116
+ end
117
+ end
118
+
119
+ # A concrete model. `ActiveRecord::Base` itself and an abstract class
120
+ # have no table to search.
121
+ def searchable_class?(klass)
122
+ ::ActiveRecord::Base > klass && !klass.abstract_class?
123
+ end
124
+
125
+ # All dependent Arel::Join nodes used in the search query.
126
+ #
127
+ # This could otherwise be done as `@object.arel.join_sources`, except
128
+ # that ActiveRecord's build_joins sets up its own JoinDependency.
129
+ # This extracts what we need to access the joins using our existing
130
+ # JoinDependency to track table aliases.
131
+ #
132
+ def join_sources
133
+ base, joins = begin
134
+ alias_tracker = @object.alias_tracker
135
+ # The mirror tree covers the relation's own joins and the ones
136
+ # Ransack added at the top level; a join Ransack added under
137
+ # another is rooted at its parent's table and only reachable
138
+ # through its stashed dependency, so those are handed on too. A
139
+ # dependency whose nodes are already in the tree merges with them.
140
+ stashed_joins = (@object.joins_values + @object.left_outer_joins_values).grep(JoinDependency)
141
+ constraints = @join_dependency.join_constraints(stashed_joins, alias_tracker, @object.references_values)
142
+
143
+ [
144
+ Arel::SelectManager.new(@object.table),
145
+ constraints
146
+ ]
147
+ end
148
+ joins.each do |aliased_join|
149
+ base.from(aliased_join)
150
+ end
151
+ base.join_sources
152
+ end
153
+
154
+ def alias_tracker
155
+ @join_dependency.send(:alias_tracker)
156
+ end
157
+
158
+ def lock_association(association)
159
+ @lock_associations << association
160
+ end
161
+
162
+ # Drops a join Ransack added, when a negative condition on a collection
163
+ # replaces it with a correlated subquery. A join the relation already
164
+ # had, or one another condition needs, is locked and stays.
165
+ def remove_association(association)
166
+ return if @lock_associations.include?(association)
167
+
168
+ @associations_pot.delete(association)
169
+ @base.children.delete_if { |child| child.equal?(association) }
170
+ @object.left_outer_joins_values = @object.left_outer_joins_values.reject { |jd|
171
+ jd.is_a?(JoinDependency) &&
172
+ jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
173
+ }
174
+ end
175
+
176
+ # Build an Arel subquery that selects keys for the top query,
177
+ # drawn from the first join association's foreign_key.
178
+ #
179
+ # Example: for an Article that has_and_belongs_to_many Tags
180
+ #
181
+ # context = Article.search.context
182
+ # attribute = Attribute.new(context, "tags_name").tap do |a|
183
+ # context.bind(a, a.name)
184
+ # end
185
+ # context.build_correlated_subquery(attribute.parent).to_sql
186
+ #
187
+ # # SELECT "articles_tags"."article_id" FROM "articles_tags"
188
+ # # INNER JOIN "tags" ON "tags"."id" = "articles_tags"."tag_id"
189
+ # # WHERE "articles_tags"."article_id" = "articles"."id"
190
+ #
191
+ # The WHERE condition on this query makes it invalid by itself,
192
+ # because it is correlated to the primary key on the outer query.
193
+ #
194
+ def build_correlated_subquery(association)
195
+ join_constraints = extract_joins(association)
196
+ join_root = join_constraints.shift
197
+ correlated_key = extract_correlated_key(join_root)
198
+ subquery = Arel::SelectManager.new(association.base_klass)
199
+ subquery.from(join_root.left)
200
+ subquery.project(correlated_key)
201
+ join_constraints.each do |j|
202
+ subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
203
+ end
204
+
205
+ # Handle polymorphic associations where correlated_key is an array
206
+ if correlated_key.is_a?(Array)
207
+ # For polymorphic associations, we need to add conditions for both the foreign key and type
208
+ correlated_key.each_with_index do |key, index|
209
+ if index == 0
210
+ # This is the foreign key
211
+ subquery = subquery.where(key.eq(primary_key))
212
+ else
213
+ # This is the type key, which should be equal to the model name
214
+ subquery = subquery.where(key.eq(@klass.name))
215
+ end
216
+ end
217
+ else
218
+ # Original behavior for non-polymorphic associations
219
+ subquery = subquery.where(correlated_key.eq(primary_key))
220
+ end
221
+
222
+ subquery
223
+ end
224
+
225
+ def primary_key
226
+ @object.table[@object.primary_key]
227
+ end
228
+
229
+ private
230
+
231
+ def extract_correlated_key(join_root)
232
+ case join_root
233
+ when Arel::Nodes::OuterJoin
234
+ # one of join_root.right/join_root.left is expected to be Arel::Nodes::On
235
+ if join_root.right.is_a?(Arel::Nodes::On)
236
+ extract_correlated_key(join_root.right.expr)
237
+ elsif join_root.left.is_a?(Arel::Nodes::On)
238
+ extract_correlated_key(join_root.left.expr)
239
+ else
240
+ raise 'Ransack encountered an unexpected arel structure'
241
+ end
242
+ when Arel::Nodes::Equality
243
+ pk = primary_key
244
+ if join_root.left.eql?(pk)
245
+ join_root.right
246
+ elsif join_root.right.eql?(pk)
247
+ join_root.left
248
+ else
249
+ nil
250
+ end
251
+ when Arel::Nodes::And
252
+ # And may have multiple children, so we need to check all, not via left/right
253
+ if join_root.children.any?
254
+ join_root.children.each do |child|
255
+ key = extract_correlated_key(child)
256
+ return key if key
257
+ end
258
+ else
259
+ extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
260
+ end
261
+ else
262
+ # eg parent was Arel::Nodes::And and the evaluated side was one of
263
+ # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
264
+ nil
265
+ end
266
+ end
267
+
268
+ def get_parent_and_attribute_name(str, parent = @base)
269
+ return [parent, nil] unless key_within_depth_limit?(str)
270
+
271
+ attr_name = nil
272
+
273
+ if ransackable_attribute?(str, klassify(parent)) ||
274
+ ransortable_attribute?(str, klassify(parent))
275
+ attr_name = str
276
+ elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
277
+ remainder = []
278
+ found_assoc = nil
279
+ while remainder.unshift(segments.pop) && segments.size > 0 &&
280
+ !found_assoc do
281
+ assoc, klass = unpolymorphize_association(
282
+ segments.join(Constants::UNDERSCORE)
283
+ )
284
+ if found_assoc = traversable_association(assoc, klass, parent)
285
+ join = build_or_find_association(
286
+ found_assoc.name, parent, klass
287
+ )
288
+ parent, attr_name = get_parent_and_attribute_name(
289
+ remainder.join(Constants::UNDERSCORE), join
290
+ )
291
+ end
292
+ end
293
+ end
294
+
295
+ [parent, attr_name]
296
+ end
297
+
298
+ def get_association(str, parent = @base)
299
+ klass = klassify parent
300
+ ransackable_association?(str, klass) &&
301
+ klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
302
+ end
303
+
304
+ # An association that can actually be walked into. A polymorphic
305
+ # association has no class of its own, so it only counts when the name
306
+ # named one with `_of_Model_type`; otherwise `from_id_or_to_id` would
307
+ # match the `from` association and ask it for a class (#1267).
308
+ def traversable_association(name, polymorphic_class, parent)
309
+ assoc = get_association(name, parent)
310
+ return nil unless assoc
311
+ return nil if assoc.polymorphic? && polymorphic_class.nil?
312
+
313
+ assoc
314
+ end
315
+
316
+ # == Joins
317
+ #
318
+ # Ransack keeps a JoinDependency of its own, @join_dependency, that
319
+ # mirrors the joins the relation will have once Active Record turns it
320
+ # into SQL: the relation's `joins` and `left_outer_joins`, the join
321
+ # dependencies already stashed on it (an eager load, a previous search)
322
+ # and the associations Ransack adds for this search. The mirror exists
323
+ # so that a condition can be bound to the table alias its join will
324
+ # have.
325
+ #
326
+ # Active Record assigns those aliases only when it builds the query,
327
+ # with a fresh alias tracker and in a fixed order: the relation's own
328
+ # `joins`, then each stashed dependency in `joins_values`, then the
329
+ # `left_outer_joins`, then the dependencies stashed among them, then
330
+ # the eager load (see Relation#build_join_buckets). The mirror seeds
331
+ # its tracker the same way and assigns in the same order, so the two
332
+ # agree. Ransack stashes the joins it adds through `left_outer_joins`,
333
+ # one dependency per association in the order they are added, which is
334
+ # where they fall in that order; a join the relation already has is
335
+ # reused rather than added again.
336
+ #
337
+ # The relation's own joins keep their tree shape in the mirror. A node
338
+ # Ransack adds sits directly under the mirror root whatever its depth,
339
+ # with its parent recorded in @associations_pot, because its stashed
340
+ # dependency is rooted at the parent's table and Active Record renders
341
+ # it on its own; placing it under the parent would render it twice.
342
+ def join_dependency(relation)
343
+ joins = relation.joins_values
344
+ left = relation.left_outer_joins_values
345
+ eager = relation.eager_loading? ? (relation.eager_load_values | relation.includes_values) : []
346
+
347
+ # The tracker seed: string joins and Arel join nodes, as in
348
+ # Relation#build_joins.
349
+ join_nodes = (joins + left).grep(Arel::Nodes::Join).uniq
350
+ string_joins = joins.grep(String).map(&:strip).uniq
351
+ alias_tracker = relation.alias_tracker(join_nodes + convert_join_strings_to_ast(relation.table, string_joins))
352
+
353
+ stashed = joins.grep(JoinDependency)
354
+ eager_jd = stashed.pop if joins.last.is_a?(JoinDependency) && joins.last.base_klass == relation.klass
355
+
356
+ mirror = JoinDependency.new(relation.klass, relation.table, named_joins(joins), Arel::Nodes::OuterJoin)
357
+ mirror.instance_variable_set(:@alias_tracker, alias_tracker)
358
+ # `where(assoc: { ... })` records the association name as a reference,
359
+ # and Active Record then aliases that join to the name (#1437).
360
+ mirror.instance_variable_set(:@references, referenced_aliases(relation))
361
+ root = mirror.instance_variable_get(:@join_root)
362
+
363
+ root.children.each { |child| adopt(mirror, root, child, Arel::Nodes::InnerJoin) }
364
+ stashed.each { |jd| graft(mirror, jd, jd.instance_variable_get(:@join_type)) }
365
+ graft(mirror, JoinDependency.new(relation.klass, relation.table, named_joins(left), Arel::Nodes::OuterJoin), Arel::Nodes::OuterJoin)
366
+ left.grep(JoinDependency).each { |jd| graft(mirror, jd, jd.instance_variable_get(:@join_type)) }
367
+ graft(mirror, eager_jd, Arel::Nodes::OuterJoin) if eager_jd
368
+ graft(mirror, JoinDependency.new(relation.klass, relation.table, eager, Arel::Nodes::OuterJoin), Arel::Nodes::OuterJoin)
369
+
370
+ mirror
371
+ end
372
+
373
+ def referenced_aliases(relation)
374
+ relation.references_values.each_with_object({}) do |table_name, references|
375
+ references[table_name.to_sym] = table_name if table_name.is_a?(Arel::Nodes::SqlLiteral)
376
+ end
377
+ end
378
+
379
+ def named_joins(values)
380
+ values.select { |join| join.is_a?(Hash) || join.is_a?(Symbol) || join.is_a?(Array) }
381
+ end
382
+
383
+ def convert_join_strings_to_ast(table, joins)
384
+ joins.map! { |join| table.create_string_join(Arel.sql(join)) unless join.blank? }
385
+ joins.compact!
386
+ joins
387
+ end
388
+
389
+ # Adds the children of a join dependency to the mirror, under the node
390
+ # they hang off, merging with what is already there the way Active
391
+ # Record's JoinDependency#walk does.
392
+ def graft(mirror, jd, join_type)
393
+ root = jd.instance_variable_get(:@join_root)
394
+ parent = mirror_node_for(mirror, root)
395
+ return unless parent
396
+
397
+ root.children.each { |child| merge(mirror, parent, child, join_type) }
398
+ end
399
+
400
+ def merge(mirror, parent, node, join_type)
401
+ existing = parent.children.find { |child| node.match?(child) }
402
+ if existing
403
+ node.table = existing.table
404
+ node.children.each { |child| merge(mirror, existing, child, join_type) }
405
+ else
406
+ parent.children << node
407
+ adopt(mirror, parent, node, join_type)
408
+ end
409
+ end
410
+
411
+ # Assigns the table aliases for a node the relation already joins, and
412
+ # every node under it, and locks them so a negative condition cannot
413
+ # remove a join that belongs to the relation.
414
+ def adopt(mirror, parent, node, join_type)
415
+ @tables_pot[node] = mirror.construct_tables_for_association!(parent, node, join_type)
416
+ @lock_associations << node
417
+ node.children.each { |child| adopt(mirror, node, child, join_type) }
418
+ end
419
+
420
+ # The mirror node a stashed dependency's root corresponds to: the
421
+ # mirror root for one rooted at the model, otherwise the node whose
422
+ # table it was built from (Ransack roots a nested join at its parent).
423
+ def mirror_node_for(mirror, root)
424
+ mirror_root = mirror.instance_variable_get(:@join_root)
425
+ return mirror_root if root.base_klass == mirror_root.base_klass && root.table.name == mirror_root.table.name
426
+
427
+ mirror_root.detect do |node|
428
+ node != mirror_root && node.base_klass == root.base_klass && node.table && node.table.name == root.table.name
429
+ end
430
+ end
431
+
432
+ def parent_of(node)
433
+ @associations_pot[node] ||
434
+ @base.detect { |candidate| candidate.children.any? { |child| child.equal?(node) } }
435
+ end
436
+
437
+ # The nodes that hang off a parent: the relation's own joins under it,
438
+ # plus the ones Ransack added under it.
439
+ def children_of(parent)
440
+ parent.children + @associations_pot.select { |_node, node_parent| node_parent.equal?(parent) }.keys
441
+ end
442
+
443
+ def build_or_find_association(name, parent = @base, klass = nil)
444
+ find_association(name, parent, klass) or build_association(name, parent, klass)
445
+ end
446
+
447
+ def find_association(name, parent = @base, klass = nil)
448
+ children_of(parent).detect do |assoc|
449
+ assoc.reflection.name == name && assoc.table &&
450
+ (!klass || assoc.reflection.klass == klass)
451
+ end
452
+ end
453
+
454
+ def build_association(name, parent = @base, klass = nil)
455
+ # Rooted at the parent's table so that, when Active Record builds
456
+ # the query, the join hangs off whatever alias the parent has there.
457
+ jd = JoinDependency.new(
458
+ parent.base_klass,
459
+ parent.table,
460
+ Join.new(name, @join_type, klass),
461
+ @join_type
462
+ )
463
+ found_association = jd.instance_variable_get(:@join_root).children.last
464
+
465
+ @associations_pot[found_association] = parent
466
+ @base.children.push found_association
467
+ @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(parent, found_association, @join_type)
468
+
469
+ # Stashed on the relation for Active Record to render; see the note
470
+ # on ordering above join_dependency.
471
+ @object = @object.left_outer_joins(jd)
472
+ found_association
473
+ end
474
+
475
+ def extract_joins(association)
476
+ parent = parent_of(association)
477
+ join_constraints = association.join_constraints_with_tables(
478
+ parent.table,
479
+ parent.base_klass,
480
+ Arel::Nodes::OuterJoin,
481
+ @join_dependency.instance_variable_get(:@alias_tracker),
482
+ @tables_pot[association]
483
+ )
484
+ join_constraints.to_a.flatten
485
+ end
486
+ end
487
+ end
488
+ end
489
+
490
+ # Resolve a Context for a model class or a relation. Registered here rather
491
+ # than hard-coded in Ransack::Context so another ORM integration can register
492
+ # its own without patching Ransack.
493
+ Ransack::Context.register do |object, options|
494
+ # Active Record may not be loaded at all when another ORM's resolver is
495
+ # the one that should answer.
496
+ next unless defined?(::ActiveRecord::Base)
497
+
498
+ case object
499
+ when Class
500
+ Ransack::ActiveRecord::Context.new(object, options) if object < ::ActiveRecord::Base
501
+ when ::ActiveRecord::Relation
502
+ Ransack::ActiveRecord::Context.new(object, options)
503
+ end
504
+ end
@@ -0,0 +1,83 @@
1
+ module Ransack
2
+ module ActiveRecord
3
+ # The SQL dialect behind a model's connection.
4
+ #
5
+ # Nearly everything Ransack generates goes through Arel, which already
6
+ # knows how to render for each database. The two exceptions are recorded
7
+ # here: whether LIKE can be made case-insensitive without wrapping the
8
+ # column in LOWER(), and which function counts characters in a string.
9
+ #
10
+ # The dialect is read from the adapter class's ancestry rather than its
11
+ # name, so an adapter that subclasses one of Rails' own — PostGIS on top
12
+ # of PostgreSQL, for example — is treated like its parent without Ransack
13
+ # having to know it exists. An adapter that subclasses none of them gets
14
+ # the generic dialect, which uses only standard SQL. `config.dialect`
15
+ # overrides the detection for an adapter that should be treated as one of
16
+ # the known dialects but does not inherit from it.
17
+ class Dialect
18
+ NAMES = %i[postgresql mysql sqlite generic].freeze
19
+
20
+ ADAPTER_ANCESTORS = {
21
+ 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter' => :postgresql,
22
+ 'ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter' => :mysql,
23
+ 'ActiveRecord::ConnectionAdapters::SQLite3Adapter' => :sqlite
24
+ }.freeze
25
+
26
+ class << self
27
+ def for(klass)
28
+ new(Ransack.options[:dialect] || detect(klass.adapter_class))
29
+ end
30
+
31
+ def detect(adapter_class)
32
+ ancestors = adapter_class.ancestors.map(&:name)
33
+ ADAPTER_ANCESTORS.each do |ancestor, dialect|
34
+ return dialect if ancestors.include?(ancestor)
35
+ end
36
+ :generic
37
+ end
38
+ end
39
+
40
+ attr_reader :name
41
+
42
+ def initialize(name)
43
+ @name = name.to_sym
44
+ unless NAMES.include?(@name)
45
+ raise ArgumentError,
46
+ "Unknown dialect #{name.inspect}; expected one of #{NAMES.map(&:inspect).join(', ')}"
47
+ end
48
+ end
49
+
50
+ NAMES.each do |dialect|
51
+ define_method(:"#{dialect}?") { @name == dialect }
52
+ end
53
+
54
+ # PostgreSQL has ILIKE, and Arel renders a `matches` that is not
55
+ # case-sensitive as ILIKE there. Elsewhere Ransack lowers both sides.
56
+ def case_insensitive_like?
57
+ postgresql?
58
+ end
59
+
60
+ # CHAR_LENGTH is the SQL standard spelling and what PostgreSQL and MySQL
61
+ # use; their LENGTH counts bytes on MySQL. SQLite has no CHAR_LENGTH,
62
+ # and neither does Oracle, while both count characters with LENGTH, so
63
+ # the generic dialect keeps LENGTH: it is the spelling the adapters
64
+ # Ransack does not know are most likely to have.
65
+ def length_function
66
+ postgresql? || mysql? ? 'CHAR_LENGTH'.freeze : 'LENGTH'.freeze
67
+ end
68
+
69
+ def ==(other)
70
+ other.is_a?(Dialect) && other.name == name
71
+ end
72
+ alias :eql? :==
73
+
74
+ def hash
75
+ name.hash
76
+ end
77
+
78
+ def inspect
79
+ "#<Ransack::ActiveRecord::Dialect #{name}>"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,75 @@
1
+ module Ransack
2
+ module ActiveRecord
3
+ # An association to join, with the join type (inner or outer) and, for a
4
+ # polymorphic belongs_to, the class to join to. Active Record's join
5
+ # syntax has no way to say either, so Ransack builds its joins from these.
6
+ class Join
7
+ include TreeNode
8
+
9
+ attr_accessor :name
10
+ attr_reader :type, :klass
11
+
12
+ def initialize(name, type = Arel::Nodes::InnerJoin, klass = nil)
13
+ @name = name
14
+ @type = convert_to_arel_join_type(type)
15
+ @klass = convert_to_class(klass) if klass
16
+ end
17
+
18
+ def klass=(klass)
19
+ @klass = convert_to_class(klass) if klass
20
+ end
21
+
22
+ def type=(type)
23
+ @type = convert_to_arel_join_type(type) if type
24
+ end
25
+
26
+ def hash
27
+ [@name, @type, @klass].hash
28
+ end
29
+
30
+ def eql?(other)
31
+ self.class == other.class &&
32
+ self.name == other.name &&
33
+ self.type == other.type &&
34
+ self.klass == other.klass
35
+ end
36
+
37
+ alias :== :eql?
38
+
39
+ def add_to_tree(hash)
40
+ hash[self] ||= {}
41
+ end
42
+
43
+ private
44
+
45
+ def convert_to_arel_join_type(type)
46
+ case type
47
+ when 'inner', :inner
48
+ Arel::Nodes::InnerJoin
49
+ when 'outer', :outer
50
+ Arel::Nodes::OuterJoin
51
+ when Class
52
+ if [Arel::Nodes::InnerJoin, Arel::Nodes::OuterJoin].include? type
53
+ type
54
+ else
55
+ raise ArgumentError, "#{type} cannot be converted to an ARel join type"
56
+ end
57
+ else
58
+ raise ArgumentError, "#{type} cannot be converted to an ARel join type"
59
+ end
60
+ end
61
+
62
+ def convert_to_class(value)
63
+ case value
64
+ when String, Symbol
65
+ Kernel.const_get(value)
66
+ when Class
67
+ value
68
+ else
69
+ raise ArgumentError, "#{value} cannot be converted to a Class"
70
+ end
71
+ end
72
+
73
+ end
74
+ end
75
+ end