activerecord-sort 8.0.0 → 8.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/CHANGELOG.md +77 -0
- data/README.md +69 -2
- data/ext/active_record/base.rb +161 -25
- data/lib/active_record/sort/version.rb +3 -1
- data/lib/active_record/sort.rb +3 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac06d1612ad8c40162ba17e9773ff79b9d3be2808a9357cb99baa213d49366dc
|
|
4
|
+
data.tar.gz: fbe32a2fbd355362ddafe5961170dcfda5ceb45bca56753fe9e909a636d0fca8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9dee845ccc33507878d09c6c1b3adb31f0e01fa94b70630086fc83fff7a472aa05fec70949d747a18da0c1c97ad54cd11963d4734cc25ea04194075f9470432f
|
|
7
|
+
data.tar.gz: 8d0d7b0feb26f2ba93a20ada61308b5b363328c3d3f31466c75143c6b239cc2ea25b5c25e4eb6d67e51bd5cd03f1964ed4771f2d4e6099dd2a419f0e52c71cae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [8.1.0] - 2026-10-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `#resort`, which sorts like `#sort` but replaces any order the relation
|
|
8
|
+
already has, as `reorder` does for `order`, including the order of an
|
|
9
|
+
association's or default scope. `Property.order(:name).resort(:id)` orders
|
|
10
|
+
by `id` alone, and `resort(nil)` removes the order as `reorder(nil)` does
|
|
11
|
+
(a Rails 8.2 `default_order` still applies, as it does after
|
|
12
|
+
`reorder(nil)`).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Aggregates on a relation sort now honor `limit` and `offset`. The
|
|
17
|
+
subquery that restricts them to the sorted records dropped the sort's
|
|
18
|
+
`GROUP BY` and `ORDER BY` but kept the `LIMIT`, so the limit applied to
|
|
19
|
+
the join's fanned-out, unordered rows: `Property.sort(addresses:
|
|
20
|
+
:id).limit(2).count` returned 1 where two records load, and `sum` added
|
|
21
|
+
up whichever records those rows happened to belong to. The subquery now
|
|
22
|
+
keeps the group and order, so it selects the same records loading would.
|
|
23
|
+
- Aggregates on a relation sort no longer put back a default scope the
|
|
24
|
+
relation removed. The outer query restricting them to the sorted records
|
|
25
|
+
started from the model, re-applying its `default_scope`, so with
|
|
26
|
+
`default_scope { where(active: true) }`,
|
|
27
|
+
`Property.unscoped.sort(addresses: :id).count` counted only the active
|
|
28
|
+
records while loading returned them all. A relation that keeps its
|
|
29
|
+
default scope still counts within it.
|
|
30
|
+
- Relation sorts now order by the association's own join when its table is
|
|
31
|
+
joined more than once. Rails aliases the later joins of a table — a
|
|
32
|
+
second `belongs_to` to the same model, a scoped `has_many`, a
|
|
33
|
+
self-referential association, or a table the caller already joined —
|
|
34
|
+
but the sort always named the plain table, so it silently ordered by
|
|
35
|
+
another join: `Property.joins(:landlord).sort(manager: :name)` ordered
|
|
36
|
+
by the landlord, `Property.sort(children: :name)` by each record's own
|
|
37
|
+
name, and `Property.sort(manager: :name).sort(landlord: :name)` dropped
|
|
38
|
+
the second sort as a duplicate of the first. The sort now finds the
|
|
39
|
+
alias Rails gave its join when the query is built, so it follows joins
|
|
40
|
+
added after it too. If that join has been removed (e.g.
|
|
41
|
+
`unscope(:left_outer_joins)`), building the query raises
|
|
42
|
+
`ActiveRecord::Sort::InvalidSort` instead of ordering by whichever join
|
|
43
|
+
has the table's name.
|
|
44
|
+
|
|
45
|
+
## [8.0.1] - 2026-09-04
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- Enabled `# frozen_string_literal: true`, so the gem no longer allocates a
|
|
50
|
+
new String for each literal it evaluates.
|
|
51
|
+
- An unrecognized value in the nulls position now raises
|
|
52
|
+
`ActiveRecord::Sort::InvalidSort` instead of being silently dropped.
|
|
53
|
+
`sort(:name => {:asc => :nulls_frist})` previously ordered by a plain
|
|
54
|
+
`ASC`, quietly returning results in the wrong null order; it now raises,
|
|
55
|
+
like any other unknown sort value. A blank nulls value still means "no
|
|
56
|
+
`NULLS` clause" rather than an invalid one — `""` (what query params
|
|
57
|
+
send) and `nil` (what JSON sends) are both absent, not wrong.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- Directions and nulls values are now matched case-insensitively wherever
|
|
62
|
+
they appear. `sort(:name => 'DESC')` already downcased, but the hash forms
|
|
63
|
+
did not, so `sort(:name => {'DESC' => 'NULLS_LAST'})` raised on the same
|
|
64
|
+
input the bare form accepted.
|
|
65
|
+
- `sort(:name => {})` now sorts ascending instead of raising. An empty hash
|
|
66
|
+
states no direction, which is what `""` (from a query string) and `nil`
|
|
67
|
+
(from JSON) already meant; the blank check simply sat after the hash
|
|
68
|
+
check and so never saw it.
|
|
69
|
+
- Sort parameters holding `nil` or a non-string key no longer raise
|
|
70
|
+
`NoMethodError`. `{"name" => {"asc" => nil}}`, `{"name" => {nil =>
|
|
71
|
+
"asc"}}` and a non-string column such as `{123 =>
|
|
72
|
+
"asc"}` — all reachable from a JSON request body, where a query string
|
|
73
|
+
would send `""` — now raise `ActiveRecord::Sort::InvalidSort`, or sort
|
|
74
|
+
normally where the value is merely absent, honoring the documented
|
|
75
|
+
contract that unfiltered params are rescuable as
|
|
76
|
+
`ActiveRecord::StatementInvalid`.
|
|
77
|
+
|
|
3
78
|
## [8.0.0] - 2026-08-27
|
|
4
79
|
|
|
5
80
|
### Breaking changes
|
|
@@ -95,5 +170,7 @@ The gem's version is now independent of the Rails version it targets.
|
|
|
95
170
|
`ActiveRecord::QueryMethods.public_instance_methods(false)` — which keeps
|
|
96
171
|
Rails' internal delegation invariants intact.
|
|
97
172
|
|
|
173
|
+
[8.1.0]: https://github.com/malomalo/activerecord-sort/releases/tag/v8.1.0
|
|
174
|
+
[8.0.1]: https://github.com/malomalo/activerecord-sort/releases/tag/v8.0.1
|
|
98
175
|
[8.0.0]: https://github.com/malomalo/activerecord-sort/releases/tag/v8.0.0
|
|
99
176
|
[7.0.0]: https://github.com/malomalo/activerecord-sort/releases/tag/v7.0.0
|
data/README.md
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`ActiveRecord::Sort` provides an easy, safe way to accept user input and order a
|
|
4
4
|
query by it. Only recognized columns and associations produce SQL — anything
|
|
5
|
-
else raises `ActiveRecord::StatementInvalid`, so
|
|
6
|
-
|
|
5
|
+
else raises `ActiveRecord::StatementInvalid`, so unfiltered request parameters
|
|
6
|
+
can't inject SQL.
|
|
7
|
+
|
|
8
|
+
Recognized is not the same as permitted, though. Every column on the model and
|
|
9
|
+
on its associations is sortable, including ones you never expose — see
|
|
10
|
+
[Restricting what can be sorted](#restricting-what-can-be-sorted).
|
|
7
11
|
|
|
8
12
|
Requirements
|
|
9
13
|
------------
|
|
@@ -110,3 +114,66 @@ it loads the records and sorts them by `<=>` — rather than building a query:
|
|
|
110
114
|
Property.all.sort # => Array of Property, sorted by <=>
|
|
111
115
|
Property.sort(nil) # => relation (unchanged), for chaining
|
|
112
116
|
```
|
|
117
|
+
|
|
118
|
+
Replacing an existing order
|
|
119
|
+
---------------------------
|
|
120
|
+
|
|
121
|
+
`#sort` adds to any order the relation already has, as `order` does.
|
|
122
|
+
`#resort` replaces it, as `reorder` does, including the order of an
|
|
123
|
+
association's or default scope:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Property.order(:name).sort(:id).to_sql
|
|
127
|
+
# => "...ORDER BY properties.name ASC, properties.id ASC"
|
|
128
|
+
|
|
129
|
+
Property.order(:name).resort(:id).to_sql
|
|
130
|
+
# => "...ORDER BY properties.id ASC"
|
|
131
|
+
|
|
132
|
+
property.addresses.resort(:id) # has_many :addresses, -> { order(:name) }
|
|
133
|
+
# => "...ORDER BY addresses.id ASC"
|
|
134
|
+
|
|
135
|
+
Property.order(:name).resort(nil) # => relation, with no ORDER BY
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With no ordering, `#resort` removes the order the way `reorder(nil)` does, so a
|
|
139
|
+
`default_order` (Rails 8.2+), which applies when a relation has no other order,
|
|
140
|
+
still applies:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
has_many :addresses, default_order: :likes
|
|
144
|
+
|
|
145
|
+
property.addresses.resort(nil) # ORDER BY likes, as with reorder(nil)
|
|
146
|
+
property.addresses.resort(:name) # ORDER BY name
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Restricting what can be sorted
|
|
150
|
+
------------------------------
|
|
151
|
+
|
|
152
|
+
`#sort` checks that a name is a real column or association — not that the
|
|
153
|
+
requester is allowed to know about it. Every column is sortable, including the
|
|
154
|
+
ones you don't select:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
User.sort(:password_digest) # valid, and it sorts
|
|
158
|
+
Post.sort(author: :reset_password_token) # so is this
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A sort reveals something about a column even though its values are never
|
|
162
|
+
returned, because the resulting order is a comparison. Someone with a row they
|
|
163
|
+
control can set their own value, see which side of it a target row lands on,
|
|
164
|
+
and narrow the value down over a series of ordinary-looking requests. Equal
|
|
165
|
+
values also sort together, which is enough to tell that two accounts share a
|
|
166
|
+
password hash.
|
|
167
|
+
|
|
168
|
+
So passing parameters straight through is safe as far as SQL injection goes,
|
|
169
|
+
but *which* columns may be sorted is an authorization question, and only your
|
|
170
|
+
application can answer it. Filter the parameters before they reach `#sort`:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
SORTABLE = %w[name created_at].freeze
|
|
174
|
+
|
|
175
|
+
Property.sort(sort_params.slice(*SORTABLE))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
[StandardAPI](https://github.com/malomalo/standardapi) does this with an ACL,
|
|
179
|
+
resolving per request which attributes a user may read and sort by.
|
data/ext/active_record/base.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_record'
|
|
2
4
|
require 'active_record/relation'
|
|
3
5
|
|
|
@@ -41,14 +43,16 @@ module ActiveRecord
|
|
|
41
43
|
ordering.each do |order|
|
|
42
44
|
order = Array(order)
|
|
43
45
|
order.each do |column_or_relation, options|
|
|
44
|
-
|
|
46
|
+
key = column_or_relation.to_s
|
|
47
|
+
|
|
48
|
+
if key == 'random'
|
|
45
49
|
resource = resource.order(Arel::Nodes::RandomOrdering.new)
|
|
46
|
-
elsif self.column_names.include?(
|
|
47
|
-
resource = resource.sort_for_column(self.arel_table[
|
|
48
|
-
elsif reflect_on_association(
|
|
49
|
-
resource = resource.sort_for_relation(
|
|
50
|
+
elsif self.column_names.include?(key)
|
|
51
|
+
resource = resource.sort_for_column(self.arel_table[key], options)
|
|
52
|
+
elsif reflect_on_association(key)
|
|
53
|
+
resource = resource.sort_for_relation(key, options)
|
|
50
54
|
else
|
|
51
|
-
raise InvalidSort.new("Unknown column #{
|
|
55
|
+
raise InvalidSort.new("Unknown column #{key}")
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
end
|
|
@@ -56,26 +60,60 @@ module ActiveRecord
|
|
|
56
60
|
resource
|
|
57
61
|
end
|
|
58
62
|
|
|
63
|
+
# Like #sort, but replaces any order the relation already has, as
|
|
64
|
+
# +reorder+ does for +order+, including the order of an association's or
|
|
65
|
+
# default scope:
|
|
66
|
+
#
|
|
67
|
+
# Property.order(:name).resort(:id)
|
|
68
|
+
# # => "...ORDER BY properties.id ASC"
|
|
69
|
+
#
|
|
70
|
+
# With no ordering, or a blank one, its order is removed, as
|
|
71
|
+
# +reorder(nil)+ does. A +default_order+ (Rails 8.2+) still applies when
|
|
72
|
+
# the relation has no other order, as it does after +reorder(nil)+.
|
|
73
|
+
def resort(*ordering)
|
|
74
|
+
resource = reorder(nil)
|
|
75
|
+
ordering.empty? ? resource : resource.sort(*ordering)
|
|
76
|
+
end
|
|
77
|
+
|
|
59
78
|
# Normalizes per-column sort options into [direction, nulls]. A blank
|
|
60
79
|
# direction — a bare column, or "" as query params often produce —
|
|
61
|
-
# means
|
|
62
|
-
# TODO: probably don't need to cast to sym
|
|
80
|
+
# means 'asc'; a blank or nil value means no NULLS clause.
|
|
63
81
|
def sort_direction_and_nulls(options)
|
|
64
|
-
if options.
|
|
65
|
-
[
|
|
66
|
-
elsif options.
|
|
67
|
-
|
|
82
|
+
if options.blank?
|
|
83
|
+
['asc', nil]
|
|
84
|
+
elsif options.is_a?(Hash) || options.class.name == "ActionController::Parameters"
|
|
85
|
+
direction = options.keys.first.to_s.downcase
|
|
86
|
+
|
|
87
|
+
if direction == 'asc' || direction == 'desc'
|
|
88
|
+
[direction, sort_nulls(options.values.first)]
|
|
89
|
+
else
|
|
90
|
+
[direction, nil]
|
|
91
|
+
end
|
|
68
92
|
else
|
|
69
|
-
[options.to_s.downcase
|
|
93
|
+
[options.to_s.downcase, nil]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Cast on the way out: Arel's visitor matches the nulls value with
|
|
98
|
+
# `case o.nulls when :nulls_first`, so this is the one value that has
|
|
99
|
+
# to reach the node as a symbol.
|
|
100
|
+
def sort_nulls(value)
|
|
101
|
+
return nil if value.blank?
|
|
102
|
+
|
|
103
|
+
nulls = value.to_s.downcase
|
|
104
|
+
if nulls != 'nulls_first' && nulls != 'nulls_last'
|
|
105
|
+
raise InvalidSort.new("Unknown nulls ordering #{value}")
|
|
70
106
|
end
|
|
107
|
+
|
|
108
|
+
nulls.to_sym
|
|
71
109
|
end
|
|
72
110
|
|
|
73
111
|
def sort_for_column(column, options)
|
|
74
112
|
direction, nulls = sort_direction_and_nulls(options)
|
|
75
113
|
|
|
76
|
-
if direction ==
|
|
114
|
+
if direction == 'desc'
|
|
77
115
|
self.order(Arel::Nodes::Descending.new(column, nulls))
|
|
78
|
-
elsif direction ==
|
|
116
|
+
elsif direction == 'asc'
|
|
79
117
|
self.order(Arel::Nodes::Ascending.new(column, nulls))
|
|
80
118
|
else
|
|
81
119
|
raise InvalidSort.new("Unknown ordering #{direction}")
|
|
@@ -117,18 +155,17 @@ module ActiveRecord
|
|
|
117
155
|
# A collection (has_many / has_and_belongs_to_many) wraps the
|
|
118
156
|
# attribute in an Arel::Attributes::Relation so the aggregate
|
|
119
157
|
# references the joined table correctly; a singular association
|
|
120
|
-
# (belongs_to / has_one) uses the plain attribute.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
end
|
|
158
|
+
# (belongs_to / has_one) uses the plain attribute. Either way the
|
|
159
|
+
# attribute is a JoinedAttribute, pointed at the table (or alias)
|
|
160
|
+
# the association's join actually uses once the query is built.
|
|
161
|
+
column = JoinedAttribute.new(relation, column_name)
|
|
162
|
+
column = Arel::Attributes::Relation.new(column, relation.name) if relation.collection?
|
|
126
163
|
|
|
127
164
|
direction, nulls = sort_direction_and_nulls(column_options)
|
|
128
165
|
|
|
129
|
-
order = if direction ==
|
|
166
|
+
order = if direction == 'desc'
|
|
130
167
|
Arel::Nodes::Descending.new(column.maximum, nulls)
|
|
131
|
-
elsif direction ==
|
|
168
|
+
elsif direction == 'asc'
|
|
132
169
|
Arel::Nodes::Ascending.new(column.minimum, nulls)
|
|
133
170
|
else
|
|
134
171
|
raise InvalidSort.new("Unknown ordering #{direction}")
|
|
@@ -158,6 +195,100 @@ module ActiveRecord
|
|
|
158
195
|
end
|
|
159
196
|
|
|
160
197
|
module Sort
|
|
198
|
+
# A column of a sorted association. Rails aliases a table it joins more
|
|
199
|
+
# than once — a second belongs_to to the same model, a scoped has_many,
|
|
200
|
+
# a self-join, or a table the caller already joined — and which join
|
|
201
|
+
# gets the alias depends on every join in the final query, so the sort
|
|
202
|
+
# can't name the table up front. This stands in for the column until
|
|
203
|
+
# the query is built, when JoinAliases swaps in the association's
|
|
204
|
+
# actual table. The reflection is part of its identity, so sorts on two
|
|
205
|
+
# associations to the same table aren't deduplicated as one.
|
|
206
|
+
class JoinedAttribute < Arel::Attributes::Attribute
|
|
207
|
+
attr_reader :reflection
|
|
208
|
+
|
|
209
|
+
def initialize(reflection, name)
|
|
210
|
+
super(reflection.klass.arel_table, name)
|
|
211
|
+
@reflection = reflection
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def eql?(other)
|
|
215
|
+
super && reflection == other.reflection
|
|
216
|
+
end
|
|
217
|
+
alias == eql?
|
|
218
|
+
|
|
219
|
+
def hash
|
|
220
|
+
[super, reflection].hash
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
module JoinAliases
|
|
225
|
+
def construct_join_dependency(associations, join_type) # :nodoc:
|
|
226
|
+
join_dependency = super
|
|
227
|
+
@sort_join_dependencies << join_dependency if @sort_join_dependencies
|
|
228
|
+
join_dependency
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
# Builds the query, recording the join dependencies Rails constructs
|
|
234
|
+
# for it, then points each JoinedAttribute in the ORDER BY at the
|
|
235
|
+
# table its association was joined as. The orders are rebuilt rather
|
|
236
|
+
# than modified: order nodes are shared with every relation chained
|
|
237
|
+
# from this one, and those can join differently.
|
|
238
|
+
def build_arel(...)
|
|
239
|
+
return super if order_values.empty?
|
|
240
|
+
|
|
241
|
+
@sort_join_dependencies = []
|
|
242
|
+
arel = super
|
|
243
|
+
# Plus those built beforehand, as eager loading and merge do.
|
|
244
|
+
join_dependencies = @sort_join_dependencies +
|
|
245
|
+
(joins_values + left_outer_joins_values).grep(ActiveRecord::Associations::JoinDependency)
|
|
246
|
+
arel.ast.orders.map! { |order| resolve_sort_joins(order, join_dependencies) }
|
|
247
|
+
arel
|
|
248
|
+
ensure
|
|
249
|
+
@sort_join_dependencies = nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def resolve_sort_joins(node, join_dependencies)
|
|
253
|
+
case node
|
|
254
|
+
when JoinedAttribute
|
|
255
|
+
table = sort_join_table(node.reflection, join_dependencies)
|
|
256
|
+
raise InvalidSort.new("Can't find the join for #{node.reflection.name} to sort by") unless table
|
|
257
|
+
table[node.name]
|
|
258
|
+
when Arel::Attributes::Relation
|
|
259
|
+
rebuild_sort_node(node, :relation, resolve_sort_joins(node.relation, join_dependencies))
|
|
260
|
+
when Arel::Nodes::Unary
|
|
261
|
+
rebuild_sort_node(node, :expr, resolve_sort_joins(node.expr, join_dependencies))
|
|
262
|
+
when Arel::Nodes::Function
|
|
263
|
+
rebuild_sort_node(node, :expressions, node.expressions.map { |e| resolve_sort_joins(e, join_dependencies) })
|
|
264
|
+
else
|
|
265
|
+
node
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def rebuild_sort_node(node, attribute, value)
|
|
270
|
+
return node if node.public_send(attribute) == value
|
|
271
|
+
|
|
272
|
+
node = node.dup
|
|
273
|
+
node.public_send(:"#{attribute}=", value)
|
|
274
|
+
node
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# The table of a top-level join of the association. Every join of it
|
|
278
|
+
# (the sort's own LEFT JOIN, the caller's, an eager load's) has the
|
|
279
|
+
# same conditions, so the first found will do.
|
|
280
|
+
def sort_join_table(reflection, join_dependencies)
|
|
281
|
+
join_dependencies.each do |join_dependency|
|
|
282
|
+
join_dependency.send(:join_root).children.each do |join|
|
|
283
|
+
if join.table && join.reflection.name == reflection.name && join.reflection.active_record == reflection.active_record
|
|
284
|
+
return join.table
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
nil
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
161
292
|
module Calculations
|
|
162
293
|
# sort_for_relation groups by the primary key and LEFT JOINs the
|
|
163
294
|
# relation, so aggregates would see grouped, fanned-out rows: count
|
|
@@ -167,10 +298,14 @@ module ActiveRecord
|
|
|
167
298
|
# answer: compute it over the base table restricted to the sorted
|
|
168
299
|
# relation's (distinct) primary keys. User joins and conditions still
|
|
169
300
|
# apply inside the subquery, and a user-added group falls through to
|
|
170
|
-
# the standard grouped behavior.
|
|
301
|
+
# the standard grouped behavior. The subquery keeps the group and
|
|
302
|
+
# order so a limit or offset picks the same records loading would,
|
|
303
|
+
# rather than counting off fanned-out, unordered rows. The outer query
|
|
304
|
+
# is unscoped: the subquery already carries whatever default scope the
|
|
305
|
+
# relation kept, and must not have one it removed put back.
|
|
171
306
|
def calculate(operation, column_name)
|
|
172
307
|
if @sorted_by_relation && group_values == [klass.arel_table[klass.primary_key]]
|
|
173
|
-
klass.where(klass.primary_key =>
|
|
308
|
+
klass.unscoped.where(klass.primary_key => except(:select).select(klass.primary_key))
|
|
174
309
|
.calculate(operation, column_name)
|
|
175
310
|
else
|
|
176
311
|
super
|
|
@@ -181,3 +316,4 @@ module ActiveRecord
|
|
|
181
316
|
end
|
|
182
317
|
|
|
183
318
|
ActiveRecord::Relation.prepend(ActiveRecord::Sort::Calculations)
|
|
319
|
+
ActiveRecord::Relation.prepend(ActiveRecord::Sort::JoinAliases)
|
data/lib/active_record/sort.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_record'
|
|
2
4
|
require 'arel/extensions'
|
|
3
5
|
|
|
4
6
|
require File.expand_path(File.join(__FILE__, '../../../ext/active_record/base'))
|
|
5
7
|
|
|
6
8
|
ActiveRecord::QueryMethods.prepend(ActiveRecord::Sort)
|
|
7
|
-
ActiveRecord::Querying.delegate :sort, to: :all
|
|
9
|
+
ActiveRecord::Querying.delegate :sort, :resort, to: :all
|