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.
- checksums.yaml +4 -4
- data/README.md +13 -27
- data/lib/ransack/active_record/base.rb +154 -0
- data/lib/ransack/active_record/context.rb +504 -0
- data/lib/ransack/active_record/dialect.rb +83 -0
- data/lib/ransack/active_record/join.rb +75 -0
- data/lib/ransack/active_record/join_association.rb +104 -0
- data/lib/ransack/active_record/join_dependency.rb +123 -0
- data/lib/ransack/active_record/reflection.rb +18 -0
- data/lib/ransack/active_record/tree_node.rb +13 -0
- data/lib/ransack/active_record.rb +44 -9
- data/lib/ransack/configuration.rb +49 -1
- data/lib/ransack/constants.rb +52 -0
- data/lib/ransack/context.rb +159 -35
- data/lib/ransack/helpers/form_builder.rb +30 -25
- data/lib/ransack/nodes/bindable.rb +0 -1
- data/lib/ransack/nodes/condition.rb +215 -73
- data/lib/ransack/nodes/grouping.rb +33 -11
- data/lib/ransack/nodes/sort.rb +1 -1
- data/lib/ransack/nodes/value.rb +8 -1
- data/lib/ransack/predicate.rb +8 -1
- data/lib/ransack/search.rb +52 -9
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack.rb +4 -1
- metadata +11 -13
- data/lib/polyamorous/activerecord/join_association.rb +0 -70
- data/lib/polyamorous/activerecord/join_association_7_2.rb +0 -55
- data/lib/polyamorous/activerecord/join_dependency.rb +0 -102
- data/lib/polyamorous/activerecord/reflection.rb +0 -11
- data/lib/polyamorous/join.rb +0 -70
- data/lib/polyamorous/polyamorous.rb +0 -27
- data/lib/polyamorous/swapping_reflection_class.rb +0 -11
- data/lib/polyamorous/tree_node.rb +0 -7
- data/lib/ransack/adapters/active_record/base.rb +0 -152
- 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
|