ransack 6.0.1 → 6.1.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 +43 -68
- data/lib/ransack/active_record/context.rb +207 -75
- data/lib/ransack/active_record/join_association.rb +8 -17
- data/lib/ransack/active_record/join_dependency.rb +163 -9
- data/lib/ransack/configuration.rb +25 -2
- data/lib/ransack/context.rb +55 -7
- data/lib/ransack/errors.rb +13 -0
- data/lib/ransack/invalid_search_error.rb +3 -3
- data/lib/ransack/locale/ar.yml +35 -0
- data/lib/ransack/locale/az.yml +36 -1
- data/lib/ransack/locale/bg.yml +35 -0
- data/lib/ransack/locale/ca.yml +36 -1
- data/lib/ransack/locale/cs.yml +35 -0
- data/lib/ransack/locale/da.yml +35 -0
- data/lib/ransack/locale/de.yml +36 -1
- data/lib/ransack/locale/el.yml +36 -1
- data/lib/ransack/locale/en.yml +35 -0
- data/lib/ransack/locale/es.yml +36 -1
- data/lib/ransack/locale/fa.yml +35 -0
- data/lib/ransack/locale/fi.yml +35 -0
- data/lib/ransack/locale/fr.yml +35 -0
- data/lib/ransack/locale/hu.yml +35 -0
- data/lib/ransack/locale/id.yml +35 -0
- data/lib/ransack/locale/it.yml +35 -0
- data/lib/ransack/locale/ja.yml +35 -0
- data/lib/ransack/locale/ko.yml +35 -0
- data/lib/ransack/locale/nl.yml +35 -0
- data/lib/ransack/locale/pt-BR.yml +35 -0
- data/lib/ransack/locale/ro.yml +35 -0
- data/lib/ransack/locale/ru.yml +35 -0
- data/lib/ransack/locale/sk.yml +35 -0
- data/lib/ransack/locale/sv.yml +35 -0
- data/lib/ransack/locale/tr.yml +36 -1
- data/lib/ransack/locale/uk.yml +35 -0
- data/lib/ransack/locale/zh-CN.yml +35 -0
- data/lib/ransack/locale/zh-TW.yml +35 -0
- data/lib/ransack/nodes/condition.rb +168 -42
- data/lib/ransack/nodes/grouping.rb +8 -17
- data/lib/ransack/nodes/node.rb +23 -1
- data/lib/ransack/nodes/sort.rb +7 -4
- data/lib/ransack/nodes/value.rb +6 -0
- data/lib/ransack/predicate.rb +19 -6
- data/lib/ransack/search.rb +21 -10
- data/lib/ransack/translate.rb +43 -35
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack.rb +2 -1
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 179b4004dbf19b3b72ebb685877fa5648b8f14b6180f0bf65cc03149be416ca6
|
|
4
|
+
data.tar.gz: 236e4f6a442ebc4dbd6abd0acb2824d6aff7ac56bff553b3f44bd3b473d766c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d19b1a2f55526e8c2bce3f68cdf4c8844d4ae5d6996d0a843f145a48682506157b9aed2578f40cc9b839f31b0a4faea7a65abdfca5da98a43a26b1dae2cb77f
|
|
7
|
+
data.tar.gz: dacca9e1ecd1ba15e58ca2bd92d87d2d491e3e4326180248d79aa62fb486dc1eb0700459dc86ef6ed2154b4def13ae1e65fcad0d0a0f40f7cfc7284fe1c94607
|
data/README.md
CHANGED
|
@@ -1,88 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Ransack
|
|
2
|
+
|
|
3
|
+
> Search everywhere and in a hurry!
|
|
2
4
|
|
|
3
5
|
[](https://github.com/activerecord-hackery/ransack/actions)
|
|
4
6
|
[](http://badge.fury.io/rb/ransack)
|
|
5
|
-
[](https://deepwiki.com/activerecord-hackery/ransack)
|
|
6
|
-
[](https://codeclimate.com/github/activerecord-hackery/ransack)
|
|
7
|
-
|
|
8
|
-
# Introduction
|
|
9
|
-
|
|
10
|
-
Ransack will help you easily add **searching to your Rails application**, without any additional dependencies.
|
|
11
|
-
|
|
12
|
-
There are advanced searching solutions around, like ElasticSearch or Algolia. **Ransack** will do the job for many Rails websites, without the need to run additional infrastructure or work in a different language. With Ransack you do it all with standard Ruby and ERB.
|
|
13
|
-
|
|
14
|
-
Ready to move beyond the basics? Use **advanced features** like i18n and extensive configuration options.
|
|
15
7
|
|
|
16
|
-
Ransack
|
|
8
|
+
Ransack adds **searching and sorting to your Rails application** with plain Ruby
|
|
9
|
+
and ERB, without a search server or any other infrastructure.
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
Ransack is supported for Rails 8.1 on Ruby 3.4 and later, on Postgres, MySQL, Trilogy and SQLite.
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```ruby title='Gemfile'
|
|
13
|
+
```ruby
|
|
23
14
|
gem 'ransack'
|
|
24
15
|
```
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```ruby title='Gemfile'
|
|
31
|
-
gem 'ransack', :github => 'activerecord-hackery/ransack', :branch => 'main'
|
|
17
|
+
```ruby
|
|
18
|
+
@q = Post.ransack(params[:q])
|
|
19
|
+
@posts = @q.result
|
|
32
20
|
```
|
|
33
21
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## Issue tracker
|
|
41
|
-
|
|
42
|
-
* Before filing an issue, please read the [Contributing Guide](CONTRIBUTING.md).
|
|
43
|
-
* File an issue if a bug is caused by Ransack, is new (has not already been reported), and _can be reproduced from the information you provide_.
|
|
44
|
-
* Please consider creating a pull request with a failing test that demonstrates the problem.
|
|
45
|
-
* Contributions are welcome. :smiley:
|
|
46
|
-
* Please do not use the issue tracker for personal support requests. Stack Overflow or [GitHub Discussions](https://github.com/activerecord-hackery/ransack/discussions) is a better place for that where a wider community can help you!
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
## Contributions
|
|
50
|
-
|
|
51
|
-
To support the project:
|
|
52
|
-
|
|
53
|
-
* Use Ransack in your apps, and let us know if you encounter anything that's
|
|
54
|
-
broken or missing. A failing spec to demonstrate the issue is awesome. A pull
|
|
55
|
-
request with passing tests is even better!
|
|
56
|
-
* Before filing an issue or pull request, be sure to read and follow the
|
|
57
|
-
[Contributing Guide](CONTRIBUTING.md).
|
|
58
|
-
* Please use Stack Overflow or [GitHub Discussions](https://github.com/activerecord-hackery/ransack/discussions) for questions or discussion not
|
|
59
|
-
directly related to bug reports, pull requests, or documentation improvements.
|
|
60
|
-
* Spread the word on social media if Ransack's been useful
|
|
61
|
-
to you. The more people who are using the project, the quicker we can find and
|
|
62
|
-
fix bugs!
|
|
22
|
+
```erb
|
|
23
|
+
<%= search_form_for @q do |f| %>
|
|
24
|
+
<%= f.search_field :title_cont %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<%= sort_link(@q, :published_at) %>
|
|
27
|
+
```
|
|
63
28
|
|
|
64
|
-
##
|
|
29
|
+
## Documentation
|
|
65
30
|
|
|
66
|
-
|
|
31
|
+
**[activerecord-hackery.github.io/ransack](https://activerecord-hackery.github.io/ransack/)**:
|
|
32
|
+
[getting started](https://activerecord-hackery.github.io/ransack/getting-started/),
|
|
33
|
+
[authorization](https://activerecord-hackery.github.io/ransack/getting-started/authorization/),
|
|
34
|
+
[predicates](https://activerecord-hackery.github.io/ransack/getting-started/search-matches/),
|
|
35
|
+
[upgrading](https://activerecord-hackery.github.io/ransack/getting-started/upgrading/)
|
|
36
|
+
and more. The site is built from the Markdown under [`docs/`](docs/).
|
|
67
37
|
|
|
68
|
-
|
|
38
|
+
## Getting help and contributing
|
|
69
39
|
|
|
70
|
-
|
|
40
|
+
* Questions belong in [GitHub Discussions](https://github.com/activerecord-hackery/ransack/discussions).
|
|
41
|
+
* Reproducible bugs belong in [Issues](https://github.com/activerecord-hackery/ransack/issues).
|
|
42
|
+
Read the [Contributing Guide](CONTRIBUTING.md) first; a failing test is the best bug report.
|
|
43
|
+
* Report security vulnerabilities privately, through GitHub
|
|
44
|
+
[private vulnerability reporting](https://github.com/activerecord-hackery/ransack/security/advisories/new)
|
|
45
|
+
or the [Tidelift security contact](https://tidelift.com/security). See the
|
|
46
|
+
[Security Policy](.github/SECURITY.md) for supported versions.
|
|
71
47
|
|
|
72
|
-
|
|
48
|
+
Ransack is maintained by volunteers. If it is useful to you, please consider
|
|
49
|
+
supporting us via [Open Collective](https://opencollective.com/ransack), or
|
|
50
|
+
through the [Tidelift Subscription](https://tidelift.com/subscription/pkg/rubygems-ransack?utm_source=rubygems-ransack&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
|
51
|
+
for commercial support.
|
|
73
52
|
|
|
74
53
|
## Contributors
|
|
75
54
|
|
|
76
|
-
Ransack
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Alumni Maintainers
|
|
84
|
-
- [Jon Atack](http://twitter.com/jonatack)
|
|
85
|
-
- [Ryan Bigg](http://twitter.com/ryanbigg)
|
|
86
|
-
|
|
87
|
-
This project exists thanks to all the people who contribute. <img src="https://opencollective.com/ransack/contributors.svg?width=890&button=false" />
|
|
55
|
+
Ransack is maintained by [Sean Carroll](https://github.com/scarroll32),
|
|
56
|
+
[Deivid Rodriguez](https://github.com/deivid-rodriguez),
|
|
57
|
+
[Greg Molnar](https://github.com/gregmolnar) and
|
|
58
|
+
[a great group of contributors](https://github.com/activerecord-hackery/ransack/graphs/contributors).
|
|
59
|
+
It was created by [Ernie Miller](http://twitter.com/erniemiller). See
|
|
60
|
+
[Community and support](https://activerecord-hackery.github.io/ransack/project/community/)
|
|
61
|
+
for the full list.
|
|
88
62
|
|
|
63
|
+
<img src="https://opencollective.com/ransack/contributors.svg?width=890&button=false" />
|
|
@@ -20,23 +20,36 @@ module Ransack
|
|
|
20
20
|
# Attributes API rather than the schema column, so that
|
|
21
21
|
# `attribute :starts_at, :datetime` on a date column is honoured (#1028);
|
|
22
22
|
# a column with no attribute override reports its column type.
|
|
23
|
+
#
|
|
24
|
+
# There is no separate check that the table exists: the schema cache
|
|
25
|
+
# does not list temporary tables, and matches names case-sensitively
|
|
26
|
+
# where SQLite does not, so it rejected tables Active Record queries
|
|
27
|
+
# fine (#1770, #657, #668). A table that really is missing fails when
|
|
28
|
+
# the model loads its columns, with Active Record's own error.
|
|
23
29
|
def type_for(attr)
|
|
24
30
|
return nil unless attr && attr.valid?
|
|
25
|
-
|
|
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
|
|
31
|
+
name = attr.arel_attribute.name.to_s
|
|
32
32
|
return nil unless attr.klass.attribute_types.key?(name)
|
|
33
33
|
|
|
34
34
|
attr.klass.type_for_attribute(name).type
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Evaluating a search leaves the context as it was, so the search can
|
|
38
|
+
# be evaluated again, or given another condition or sort, and still
|
|
39
|
+
# line up with the query Active Record builds (#1745, #1750).
|
|
40
|
+
#
|
|
41
|
+
# The relation returned keeps only the joins Ransack added that the
|
|
42
|
+
# search, as it stands now, reads: a condition or sort on the joined
|
|
43
|
+
# table, or one on an association reached through it. A negative
|
|
44
|
+
# condition on a collection is a correlated subquery and reads none of
|
|
45
|
+
# its own. Joins the relation already had are always kept.
|
|
37
46
|
def evaluate(search, opts = {})
|
|
38
47
|
viz = Visitor.new
|
|
39
|
-
|
|
48
|
+
predicate, used = tracking_used_associations do
|
|
49
|
+
search.sorts.each { |sort| use_association(sort.parent) if sort.valid? }
|
|
50
|
+
viz.accept(search.base)
|
|
51
|
+
end
|
|
52
|
+
relation = with_joins_used_by(@object, used).where(predicate)
|
|
40
53
|
|
|
41
54
|
if search.sorts.any?
|
|
42
55
|
relation = relation.except(:order)
|
|
@@ -74,12 +87,22 @@ module Ransack
|
|
|
74
87
|
opts[:distinct] ? relation.distinct : relation
|
|
75
88
|
end
|
|
76
89
|
|
|
90
|
+
# Asked for the same names over and over while a search is built, and
|
|
91
|
+
# answered from the allowlists, so kept per model for the life of the
|
|
92
|
+
# context and dropped with them.
|
|
77
93
|
def attribute_method?(str, klass = @klass)
|
|
78
94
|
return false unless key_within_depth_limit?(str)
|
|
79
95
|
|
|
96
|
+
cache = ((@attribute_methods ||= {})[klass] ||= {})
|
|
97
|
+
key = str.to_s
|
|
98
|
+
return cache[key] if cache.key?(key)
|
|
99
|
+
|
|
100
|
+
cache[key] = attribute_method_uncached?(key, klass)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def attribute_method_uncached?(str, klass)
|
|
80
104
|
exists = false
|
|
81
|
-
if
|
|
82
|
-
ransortable_attribute?(str, klass)
|
|
105
|
+
if allowlisted_attribute?(str, klass)
|
|
83
106
|
exists = true
|
|
84
107
|
elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
|
|
85
108
|
remainder = []
|
|
@@ -90,7 +113,7 @@ module Ransack
|
|
|
90
113
|
segments.join(Constants::UNDERSCORE)
|
|
91
114
|
)
|
|
92
115
|
if found_assoc = traversable_association(assoc, poly_class, klass)
|
|
93
|
-
exists =
|
|
116
|
+
exists = attribute_method_uncached?(
|
|
94
117
|
remainder.join(Constants::UNDERSCORE),
|
|
95
118
|
poly_class || found_assoc.klass
|
|
96
119
|
)
|
|
@@ -99,6 +122,20 @@ module Ransack
|
|
|
99
122
|
end
|
|
100
123
|
exists
|
|
101
124
|
end
|
|
125
|
+
private :attribute_method_uncached?
|
|
126
|
+
|
|
127
|
+
# A name on the allowlists is only taken for an attribute of `klass`
|
|
128
|
+
# when it is one: a column or `attribute` declaration, an
|
|
129
|
+
# `alias_attribute`, a ransacker or a `ransack_alias`. Anything else,
|
|
130
|
+
# such as `person_name` listed on Article, is left to be followed
|
|
131
|
+
# through the associations rather than bound as a missing column
|
|
132
|
+
# (#1774).
|
|
133
|
+
def allowlisted_attribute?(str, klass)
|
|
134
|
+
(ransackable_attribute?(str, klass) || ransortable_attribute?(str, klass)) &&
|
|
135
|
+
(klass.has_attribute?(str) || klass._ransackers.key?(str) ||
|
|
136
|
+
klass._ransack_aliases.key?(str))
|
|
137
|
+
end
|
|
138
|
+
private :allowlisted_attribute?
|
|
102
139
|
|
|
103
140
|
def table_for(parent)
|
|
104
141
|
parent.table
|
|
@@ -155,24 +192,36 @@ module Ransack
|
|
|
155
192
|
@join_dependency.send(:alias_tracker)
|
|
156
193
|
end
|
|
157
194
|
|
|
195
|
+
# Keeps a join, and every join it hangs off, in every evaluation,
|
|
196
|
+
# whether or not the search reads it.
|
|
158
197
|
def lock_association(association)
|
|
159
198
|
@lock_associations << association
|
|
160
199
|
end
|
|
161
200
|
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
201
|
+
# Records that the search being evaluated reads the association's
|
|
202
|
+
# joined table, so its join, and every join it hangs off, stays.
|
|
203
|
+
def use_association(association)
|
|
204
|
+
@used_associations << association if @used_associations && association
|
|
205
|
+
end
|
|
167
206
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
207
|
+
# Records that the search being evaluated reads the joins the
|
|
208
|
+
# association hangs off but not its own: a negative condition on a
|
|
209
|
+
# collection is a subquery, but one nested under another collection
|
|
210
|
+
# still needs that collection's join.
|
|
211
|
+
def use_ancestors_of(association)
|
|
212
|
+
use_association(parent_of(association)) if association
|
|
174
213
|
end
|
|
175
214
|
|
|
215
|
+
# Whether the association hangs directly off the model searched,
|
|
216
|
+
# rather than off another association.
|
|
217
|
+
def root_association?(association)
|
|
218
|
+
parent_of(association).equal?(@base)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Kept for callers of the 6.0.0 API. Evaluation now keeps only the
|
|
222
|
+
# joins the search reads, so a join nothing reads is already left out.
|
|
223
|
+
def remove_association(_association); end
|
|
224
|
+
|
|
176
225
|
# Build an Arel subquery that selects keys for the top query,
|
|
177
226
|
# drawn from the first join association's foreign_key.
|
|
178
227
|
#
|
|
@@ -192,34 +241,48 @@ module Ransack
|
|
|
192
241
|
# because it is correlated to the primary key on the outer query.
|
|
193
242
|
#
|
|
194
243
|
def build_correlated_subquery(association)
|
|
244
|
+
correlated_subquery(association).last
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# The subquery a negative condition on a collection is tested against,
|
|
248
|
+
# and the outer query's column to test: the primary key, or the
|
|
249
|
+
# association's `primary_key:` when it has one.
|
|
250
|
+
#
|
|
251
|
+
# The subquery correlates on the join condition that references the
|
|
252
|
+
# outer table, and keeps the join's other conditions: the type column
|
|
253
|
+
# of a polymorphic or STI association, the association's scope and the
|
|
254
|
+
# target's default scope. Leaving them out counted rows the association
|
|
255
|
+
# does not reach (#1747).
|
|
256
|
+
def correlated_subquery(association)
|
|
195
257
|
join_constraints = extract_joins(association)
|
|
196
258
|
join_root = join_constraints.shift
|
|
197
|
-
|
|
198
|
-
|
|
259
|
+
correlation = extract_correlation(join_root)
|
|
260
|
+
raise 'Ransack encountered an unexpected arel structure' unless correlation
|
|
261
|
+
|
|
262
|
+
inner_key, outer_key = correlated_keys(correlation)
|
|
263
|
+
subquery = Arel::SelectManager.new(association.base_klass.arel_table)
|
|
199
264
|
subquery.from(join_root.left)
|
|
200
|
-
subquery.project(
|
|
265
|
+
subquery.project(inner_key)
|
|
201
266
|
join_constraints.each do |j|
|
|
202
267
|
subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
|
|
203
268
|
end
|
|
204
269
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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))
|
|
270
|
+
subquery.where(inner_key.eq(outer_key))
|
|
271
|
+
join_conditions(join_root).each do |condition|
|
|
272
|
+
subquery.where(condition) unless condition.equal?(correlation)
|
|
220
273
|
end
|
|
221
274
|
|
|
222
|
-
subquery
|
|
275
|
+
[outer_key, subquery]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# The SQL for a correlated subquery, with bind values inlined the way
|
|
279
|
+
# Relation#to_sql renders them, since the subquery is embedded in the
|
|
280
|
+
# outer query as a string. An association or default scope condition
|
|
281
|
+
# in the subquery carries its value as a bind.
|
|
282
|
+
def subquery_sql(subquery)
|
|
283
|
+
@klass.with_connection do |connection|
|
|
284
|
+
connection.unprepared_statement { connection.to_sql(subquery) }
|
|
285
|
+
end
|
|
223
286
|
end
|
|
224
287
|
|
|
225
288
|
def primary_key
|
|
@@ -228,50 +291,61 @@ module Ransack
|
|
|
228
291
|
|
|
229
292
|
private
|
|
230
293
|
|
|
231
|
-
|
|
232
|
-
|
|
294
|
+
# The conditions in a join's ON clause, with its ANDs flattened.
|
|
295
|
+
def join_conditions(join)
|
|
296
|
+
on = [join.right, join.left].find { |node| node.is_a?(Arel::Nodes::On) }
|
|
297
|
+
on ? and_children(on.expr) : []
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def and_children(node)
|
|
301
|
+
node.is_a?(Arel::Nodes::And) ? node.children.flat_map { |child| and_children(child) } : [node]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# The equality in a join's ON clause that ties the joined table to
|
|
305
|
+
# the outer query's table.
|
|
306
|
+
def extract_correlation(node)
|
|
307
|
+
case node
|
|
233
308
|
when Arel::Nodes::OuterJoin
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
extract_correlated_key(join_root.left.expr)
|
|
239
|
-
else
|
|
240
|
-
raise 'Ransack encountered an unexpected arel structure'
|
|
241
|
-
end
|
|
309
|
+
on = [node.right, node.left].find { |side| side.is_a?(Arel::Nodes::On) }
|
|
310
|
+
raise 'Ransack encountered an unexpected arel structure' unless on
|
|
311
|
+
|
|
312
|
+
extract_correlation(on.expr)
|
|
242
313
|
when Arel::Nodes::Equality
|
|
243
|
-
|
|
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
|
|
314
|
+
node if correlated_keys(node)
|
|
251
315
|
when Arel::Nodes::And
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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)
|
|
316
|
+
node.children.each do |child|
|
|
317
|
+
correlation = extract_correlation(child)
|
|
318
|
+
return correlation if correlation
|
|
260
319
|
end
|
|
320
|
+
nil
|
|
261
321
|
else
|
|
262
|
-
# eg
|
|
263
|
-
# Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
|
|
322
|
+
# eg a Grouping, or a MultiTenant::TenantEnforcementClause
|
|
264
323
|
nil
|
|
265
324
|
end
|
|
266
325
|
end
|
|
267
326
|
|
|
327
|
+
# [inner, outer] for an equality with exactly one side on the outer
|
|
328
|
+
# query's table, or nil.
|
|
329
|
+
def correlated_keys(equality)
|
|
330
|
+
left_outer = outer_attribute?(equality.left)
|
|
331
|
+
right_outer = outer_attribute?(equality.right)
|
|
332
|
+
if left_outer && !right_outer
|
|
333
|
+
[equality.right, equality.left]
|
|
334
|
+
elsif right_outer && !left_outer
|
|
335
|
+
[equality.left, equality.right]
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def outer_attribute?(node)
|
|
340
|
+
node.is_a?(Arel::Attributes::Attribute) && node.relation.eql?(@object.table)
|
|
341
|
+
end
|
|
342
|
+
|
|
268
343
|
def get_parent_and_attribute_name(str, parent = @base)
|
|
269
344
|
return [parent, nil] unless key_within_depth_limit?(str)
|
|
270
345
|
|
|
271
346
|
attr_name = nil
|
|
272
347
|
|
|
273
|
-
if
|
|
274
|
-
ransortable_attribute?(str, klassify(parent))
|
|
348
|
+
if allowlisted_attribute?(str, klassify(parent))
|
|
275
349
|
attr_name = str
|
|
276
350
|
elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
|
|
277
351
|
remainder = []
|
|
@@ -332,7 +406,9 @@ module Ransack
|
|
|
332
406
|
# agree. Ransack stashes the joins it adds through `left_outer_joins`,
|
|
333
407
|
# one dependency per association in the order they are added, which is
|
|
334
408
|
# where they fall in that order; a join the relation already has is
|
|
335
|
-
# reused rather than added again.
|
|
409
|
+
# reused rather than added again. Their aliases are pinned, so a join
|
|
410
|
+
# added to the result later, which renders ahead of them, is aliased
|
|
411
|
+
# around them instead (see JoinDependencyExtensions#pin_search_tables).
|
|
336
412
|
#
|
|
337
413
|
# The relation's own joins keep their tree shape in the mirror. A node
|
|
338
414
|
# Ransack adds sits directly under the mirror root whatever its depth,
|
|
@@ -358,14 +434,24 @@ module Ransack
|
|
|
358
434
|
# `where(assoc: { ... })` records the association name as a reference,
|
|
359
435
|
# and Active Record then aliases that join to the name (#1437).
|
|
360
436
|
mirror.instance_variable_set(:@references, referenced_aliases(relation))
|
|
437
|
+
# Before any join is given an alias, so the joins an earlier search
|
|
438
|
+
# added keep theirs (#1776).
|
|
439
|
+
mirror.pin_search_tables(left.grep(JoinDependency))
|
|
361
440
|
root = mirror.instance_variable_get(:@join_root)
|
|
362
441
|
|
|
363
442
|
root.children.each { |child| adopt(mirror, root, child, Arel::Nodes::InnerJoin) }
|
|
364
443
|
stashed.each { |jd| graft(mirror, jd, jd.instance_variable_get(:@join_type)) }
|
|
365
444
|
graft(mirror, JoinDependency.new(relation.klass, relation.table, named_joins(left), Arel::Nodes::OuterJoin), Arel::Nodes::OuterJoin)
|
|
366
|
-
|
|
445
|
+
# The joins a search added after counting the eager load are rendered
|
|
446
|
+
# after it (JoinDependencyExtensions#eager_load_first), as long as the
|
|
447
|
+
# relation still eager loads; otherwise they keep their order. Those
|
|
448
|
+
# this search adds are counted after it too.
|
|
449
|
+
@eager_loading = !eager_jd.nil? || eager.any?
|
|
450
|
+
after_eager, before_eager = left.grep(JoinDependency).partition { |jd| @eager_loading && jd.after_eager_load }
|
|
451
|
+
before_eager.each { |jd| graft(mirror, jd, jd.instance_variable_get(:@join_type)) }
|
|
367
452
|
graft(mirror, eager_jd, Arel::Nodes::OuterJoin) if eager_jd
|
|
368
453
|
graft(mirror, JoinDependency.new(relation.klass, relation.table, eager, Arel::Nodes::OuterJoin), Arel::Nodes::OuterJoin)
|
|
454
|
+
after_eager.each { |jd| graft(mirror, jd, jd.instance_variable_get(:@join_type)) }
|
|
369
455
|
|
|
370
456
|
mirror
|
|
371
457
|
end
|
|
@@ -435,9 +521,53 @@ module Ransack
|
|
|
435
521
|
end
|
|
436
522
|
|
|
437
523
|
# The nodes that hang off a parent: the relation's own joins under it,
|
|
438
|
-
# plus the ones Ransack added under it.
|
|
524
|
+
# plus the ones Ransack added under it. The nodes Ransack added all sit
|
|
525
|
+
# under the mirror root whatever their parent, so the root's children
|
|
526
|
+
# are filtered to the relation's own; otherwise a nested `person` would
|
|
527
|
+
# be found as the root's `person` too (#1775).
|
|
439
528
|
def children_of(parent)
|
|
440
|
-
parent.children
|
|
529
|
+
parent.children.reject { |node| @associations_pot.key?(node) } +
|
|
530
|
+
@associations_pot.select { |_node, node_parent| node_parent.equal?(parent) }.keys
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Collects the associations the block's evaluation reads. Saved and
|
|
534
|
+
# restored, so an evaluation nested inside another one (a ransacker
|
|
535
|
+
# that runs a search on this context) keeps its own list.
|
|
536
|
+
def tracking_used_associations
|
|
537
|
+
outer = @used_associations
|
|
538
|
+
@used_associations = []
|
|
539
|
+
result = yield
|
|
540
|
+
[result, @used_associations]
|
|
541
|
+
ensure
|
|
542
|
+
@used_associations = outer
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# The relation without the joins Ransack added that the search does not
|
|
546
|
+
# read. A join the relation already had is not Ransack's, and stays.
|
|
547
|
+
def with_joins_used_by(relation, used)
|
|
548
|
+
needed = with_ancestors(used + @lock_associations).map(&:object_id)
|
|
549
|
+
unused = @associations_pot.keys.map(&:object_id) - needed
|
|
550
|
+
return relation if unused.empty?
|
|
551
|
+
|
|
552
|
+
relation = relation.dup
|
|
553
|
+
relation.left_outer_joins_values = relation.left_outer_joins_values.reject { |jd|
|
|
554
|
+
jd.is_a?(JoinDependency) &&
|
|
555
|
+
(children = jd.instance_variable_get(:@join_root).children).size == 1 &&
|
|
556
|
+
unused.include?(children.first.object_id)
|
|
557
|
+
}
|
|
558
|
+
relation
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def with_ancestors(associations)
|
|
562
|
+
associations.flat_map do |association|
|
|
563
|
+
chain = []
|
|
564
|
+
node = association
|
|
565
|
+
while node && !node.equal?(@base)
|
|
566
|
+
chain << node
|
|
567
|
+
node = parent_of(node)
|
|
568
|
+
end
|
|
569
|
+
chain
|
|
570
|
+
end
|
|
441
571
|
end
|
|
442
572
|
|
|
443
573
|
def build_or_find_association(name, parent = @base, klass = nil)
|
|
@@ -460,6 +590,8 @@ module Ransack
|
|
|
460
590
|
Join.new(name, @join_type, klass),
|
|
461
591
|
@join_type
|
|
462
592
|
)
|
|
593
|
+
jd.added_by_search = true
|
|
594
|
+
jd.after_eager_load = @eager_loading
|
|
463
595
|
found_association = jd.instance_variable_get(:@join_root).children.last
|
|
464
596
|
|
|
465
597
|
@associations_pot[found_association] = parent
|
|
@@ -8,7 +8,13 @@ module Ransack
|
|
|
8
8
|
# collection uses the same aliases as the outer query.
|
|
9
9
|
module JoinAssociationExtensions
|
|
10
10
|
def self.prepended(base)
|
|
11
|
-
base.class_eval
|
|
11
|
+
base.class_eval do
|
|
12
|
+
attr_reader :join_type
|
|
13
|
+
# The tables of the joins this node renders itself, as Ransack
|
|
14
|
+
# first assigned them: its reflection chain up to the first table
|
|
15
|
+
# an earlier join already provides.
|
|
16
|
+
attr_accessor :own_tables
|
|
17
|
+
end
|
|
12
18
|
end
|
|
13
19
|
|
|
14
20
|
def initialize(reflection, children, polymorphic_class = nil, join_type = Arel::Nodes::InnerJoin)
|
|
@@ -75,7 +81,7 @@ module Ransack
|
|
|
75
81
|
|
|
76
82
|
if others && !others.empty?
|
|
77
83
|
joins.concat arel.join_sources
|
|
78
|
-
|
|
84
|
+
append_constraints(joins.last, others)
|
|
79
85
|
end
|
|
80
86
|
|
|
81
87
|
# The current table in this iteration becomes the foreign table in the next
|
|
@@ -84,21 +90,6 @@ module Ransack
|
|
|
84
90
|
|
|
85
91
|
joins
|
|
86
92
|
end
|
|
87
|
-
|
|
88
|
-
private
|
|
89
|
-
|
|
90
|
-
# Active Record's private append_constraints takes a leading connection
|
|
91
|
-
# argument in 7.2.0–7.2.2.2 and 8.0.0–8.0.2, and none in 7.2.3+, 8.0.3+
|
|
92
|
-
# and 8.1; the arity says which is loaded.
|
|
93
|
-
def append_constraints_to(join, constraints)
|
|
94
|
-
if method(:append_constraints).arity == 3
|
|
95
|
-
base_klass.with_connection do |connection|
|
|
96
|
-
append_constraints(connection, join, constraints)
|
|
97
|
-
end
|
|
98
|
-
else
|
|
99
|
-
append_constraints(join, constraints)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
93
|
end
|
|
103
94
|
end
|
|
104
95
|
end
|