activerecord-sort 7.0.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec46a72d471b80abc582726404812b56fb9a89176349d5609b9734a6227c4c61
4
- data.tar.gz: dd93e9fd01692c8113898c9c34aac125af3288a8b4d92d0be047660f396ace24
3
+ metadata.gz: '00973c2942f313280f8239187ec8c9e0a6ae444211606154c92b10c54333b109'
4
+ data.tar.gz: d00b63b7250b0b7789c9d02aa6d4654db904ffca88a99e19650715ccd9c25884
5
5
  SHA512:
6
- metadata.gz: 56688dd4bc023b5098de0792f27309452dd85d46bf56f905ae771b9ff3154d5c0ef5c9348a52e363b3a4ed20987805eb7f1e66384faf4eca40429ac126eb15ec
7
- data.tar.gz: 8950bc656939f09fb886d5c917fb12a295a3da80d6d79a59ffde629a909c82a335aea17ce3cb5fbaceb4c1e5925261e11e536853321c89e58896dca9c66e2cff
6
+ metadata.gz: e50419cf6d24fb591606d97f39d7928b16daffd65ee8d2f70e3f105508e3c5073d2bdd3d1187cc150f0ef3014e422277ba91afad44d6287e19cc3b8fd12fe784
7
+ data.tar.gz: dfc4e53f7e3f115c84c2804220e91475dd85c930759ac67af1cdf9b0c0f40d0be9327e0e789f5c15481d74837b7c9c83231311ff1c0723d4e729a0f4f813eb5a
data/CHANGELOG.md CHANGED
@@ -1,4 +1,107 @@
1
- ## [7.0.0]
1
+ # Changelog
2
+
3
+ ## [8.0.1] - 2026-09-04
4
+
5
+ ### Changed
6
+
7
+ - Enabled `# frozen_string_literal: true`, so the gem no longer allocates a
8
+ new String for each literal it evaluates.
9
+ - An unrecognized value in the nulls position now raises
10
+ `ActiveRecord::Sort::InvalidSort` instead of being silently dropped.
11
+ `sort(:name => {:asc => :nulls_frist})` previously ordered by a plain
12
+ `ASC`, quietly returning results in the wrong null order; it now raises,
13
+ like any other unknown sort value. A blank nulls value still means "no
14
+ `NULLS` clause" rather than an invalid one — `""` (what query params
15
+ send) and `nil` (what JSON sends) are both absent, not wrong.
16
+
17
+ ### Fixed
18
+
19
+ - Directions and nulls values are now matched case-insensitively wherever
20
+ they appear. `sort(:name => 'DESC')` already downcased, but the hash forms
21
+ did not, so `sort(:name => {'DESC' => 'NULLS_LAST'})` raised on the same
22
+ input the bare form accepted.
23
+ - `sort(:name => {})` now sorts ascending instead of raising. An empty hash
24
+ states no direction, which is what `""` (from a query string) and `nil`
25
+ (from JSON) already meant; the blank check simply sat after the hash
26
+ check and so never saw it.
27
+ - Sort parameters holding `nil` or a non-string key no longer raise
28
+ `NoMethodError`. `{"name" => {"asc" => nil}}`, `{"name" => {nil =>
29
+ "asc"}}` and a non-string column such as `{123 =>
30
+ "asc"}` — all reachable from a JSON request body, where a query string
31
+ would send `""` — now raise `ActiveRecord::Sort::InvalidSort`, or sort
32
+ normally where the value is merely absent, honoring the documented
33
+ contract that unfiltered params are rescuable as
34
+ `ActiveRecord::StatementInvalid`.
35
+
36
+ ## [8.0.0] - 2026-08-27
37
+
38
+ ### Breaking changes
39
+
40
+ - Relation sorts are unified across association types. Sorting by any
41
+ relation (`has_many`, `has_and_belongs_to_many`, `has_one`,
42
+ `belongs_to`) now `LEFT OUTER JOIN`s the association, groups by the
43
+ sorted table's primary key, and orders by an aggregate of the requested
44
+ column. For `has_many` sorts this changes behavior:
45
+ - each record is returned once, instead of once per associated row
46
+ (the join no longer fans out into duplicates)
47
+ - records with no associated rows are included (previously dropped by
48
+ the `INNER JOIN`)
49
+ - Descending relation sorts key each record by its largest member
50
+ (`MAX`), ascending by its smallest (`MIN`) — the member you'd expect
51
+ to see first in that direction. For records with multiple associated
52
+ rows, descending is therefore not the reverse of ascending: a record
53
+ holding both extremes sorts first in both directions.
54
+ - Sort columns are no longer added to the `SELECT`:
55
+ - loaded records keep their own attributes — a joined sort column can
56
+ no longer overwrite a same-named attribute on the base record
57
+ (previously even `id` could be clobbered)
58
+ - `pluck` and `ids` keep the sort (previously raised on
59
+ `has_and_belongs_to_many` sorts)
60
+ - a caller's `select` is left untouched
61
+ - chaining `.distinct` after a relation sort now raises: PostgreSQL
62
+ requires `ORDER BY` expressions to appear in the select list for
63
+ `SELECT DISTINCT`. It previously appeared to work while silently
64
+ deduplicating over the wrong tuple.
65
+ - A bare `belongs_to`/`has_one` sort with no direction (e.g.
66
+ `Address.sort(property: :name)`) now defaults to ascending, matching
67
+ every other sort form (previously descending).
68
+ - An unknown sort direction on a relation sort (e.g. `:dsc`) now raises
69
+ `ActiveRecord::Sort::InvalidSort` (previously sorted ascending
70
+ silently), matching column sorts.
71
+
72
+ ### Added
73
+
74
+ - Sorting by `has_and_belongs_to_many` relations.
75
+ - Unrecognized sort columns, associations, and directions raise
76
+ `ActiveRecord::Sort::InvalidSort`, a subclass of
77
+ `ActiveRecord::StatementInvalid` — so existing
78
+ `rescue ActiveRecord::StatementInvalid` handlers still catch bad sort
79
+ parameters, while callers can rescue the narrower class. Relation sort
80
+ columns are now validated up front (against the associated model's
81
+ columns) instead of only failing once the query reaches the database.
82
+ - Sorts of different types compose: they can be combined in one call
83
+ (`Property.sort(:name, tags: :name, addresses: :id)`) or chained
84
+ (`.sort(...).sort(...)`), sharing a single `GROUP BY`.
85
+ - Aggregates on a sorted relation (`count`, `sum`, `average`, `minimum`,
86
+ `maximum`) are computed over the records themselves rather than the
87
+ sort's grouped, joined rows — `count` returns the record count instead
88
+ of a per-group `Hash`, and multi-member records aren't weighted once
89
+ per member. A caller-supplied `group` still gets standard grouped
90
+ results.
91
+ - A blank direction (`''`, as query parameters often produce) is
92
+ accepted as ascending on all sort forms.
93
+ - `ActionController::Parameters` are accepted for relation sorts, and
94
+ their nulls option (`nulls_first`/`nulls_last`) is honored.
95
+
96
+ ### Fixed
97
+
98
+ - `has_and_belongs_to_many` sorts crashed with `NoMethodError` on
99
+ ActiveRecord <= 8.0 (`Function#as` mutates and returns the receiver
100
+ there).
101
+ - Combining a `has_and_belongs_to_many` sort with another relation sort
102
+ raised `PG::GroupingError`.
103
+
104
+ ## [7.0.0] - 2026-08-27
2
105
 
3
106
  The gem's version is now independent of the Rails version it targets.
4
107
 
@@ -25,4 +128,5 @@ The gem's version is now independent of the Rails version it targets.
25
128
  `ActiveRecord::QueryMethods.public_instance_methods(false)` — which keeps
26
129
  Rails' internal delegation invariants intact.
27
130
 
131
+ [8.0.0]: https://github.com/malomalo/activerecord-sort/releases/tag/v8.0.0
28
132
  [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 it's safe to pass request
6
- parameters straight through.
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
  ------------
@@ -50,22 +54,41 @@ Property.sort(id: {asc: :nulls_last}).to_sql
50
54
  # => "...ORDER BY properties.id ASC NULLS LAST"
51
55
  ```
52
56
 
53
- It can also sort on associations:
57
+ It can also sort on relations. A relation sort groups by the sorted table's
58
+ primary key — so each record appears once and records with no associated
59
+ rows are still included — and orders by an aggregate of the requested
60
+ column: `MIN` ascending or `MAX` descending, keying each record by the
61
+ member you'd expect to see first in that direction. A record with no
62
+ associated rows has a `NULL` sort key; where `NULL`s land is
63
+ database-dependent, so pass `nulls_first`/`nulls_last` to place those
64
+ records explicitly:
54
65
 
55
66
  ```ruby
56
67
  Property.sort(addresses: :id).to_sql
57
- # => "...INNER JOIN addresses ON addresses.property_id = properties.id
58
- # => " ORDER BY addresses.id ASC"
68
+ # => "SELECT properties.* FROM properties
69
+ # => " LEFT OUTER JOIN addresses ON addresses.property_id = properties.id
70
+ # => " GROUP BY properties.id
71
+ # => " ORDER BY MIN(addresses.id) ASC"
59
72
 
60
73
  Property.sort(addresses: {id: :desc}).to_sql
61
- # => "...INNER JOIN addresses ON addresses.property_id = properties.id
62
- # => " ORDER BY addresses.id DESC"
74
+ # => "...ORDER BY MAX(addresses.id) DESC"
63
75
 
64
76
  Property.sort(addresses: {id: {asc: :nulls_first}}).to_sql
65
- # => "...INNER JOIN addresses ON addresses.property_id = properties.id
66
- # => " ORDER BY addresses.id ASC NULLS FIRST"
77
+ # => "...ORDER BY MIN(addresses.id) ASC NULLS FIRST"
78
+
79
+ Property.sort(tags: :name).to_sql # has_and_belongs_to_many
80
+ # => "SELECT properties.* FROM properties
81
+ # => " LEFT OUTER JOIN properties_tags ON properties_tags.property_id = properties.id
82
+ # => " LEFT OUTER JOIN tags ON tags.id = properties_tags.tag_id
83
+ # => " GROUP BY properties.id
84
+ # => " ORDER BY MIN(tags.name) ASC"
67
85
  ```
68
86
 
87
+ A relation sort is order-only — it never adds or removes records — so
88
+ aggregates on a sorted relation (`count`, `sum`, `average`, `minimum`,
89
+ `maximum`) are computed over the records themselves, not the sort's
90
+ grouped and joined rows.
91
+
69
92
  Order randomly:
70
93
 
71
94
  ```ruby
@@ -77,9 +100,13 @@ Unrecognized columns raise, so unfiltered params can't inject SQL:
77
100
 
78
101
  ```ruby
79
102
  Property.sort(:name_or_something_unexpected)
80
- # => raises ActiveRecord::StatementInvalid
103
+ # => raises ActiveRecord::Sort::InvalidSort
81
104
  ```
82
105
 
106
+ `ActiveRecord::Sort::InvalidSort` subclasses `ActiveRecord::StatementInvalid`,
107
+ so existing `rescue ActiveRecord::StatementInvalid` handlers keep catching bad
108
+ sort parameters, while callers that want to can rescue the narrower class.
109
+
83
110
  Called with no arguments, `#sort` behaves like Ruby's `Enumerable#sort` —
84
111
  it loads the records and sorts them by `<=>` — rather than building a query:
85
112
 
@@ -87,3 +114,35 @@ it loads the records and sorts them by `<=>` — rather than building a query:
87
114
  Property.all.sort # => Array of Property, sorted by <=>
88
115
  Property.sort(nil) # => relation (unchanged), for chaining
89
116
  ```
117
+
118
+ Restricting what can be sorted
119
+ ------------------------------
120
+
121
+ `#sort` checks that a name is a real column or association — not that the
122
+ requester is allowed to know about it. Every column is sortable, including the
123
+ ones you don't select:
124
+
125
+ ```ruby
126
+ User.sort(:password_digest) # valid, and it sorts
127
+ Post.sort(author: :reset_password_token) # so is this
128
+ ```
129
+
130
+ A sort reveals something about a column even though its values are never
131
+ returned, because the resulting order is a comparison. Someone with a row they
132
+ control can set their own value, see which side of it a target row lands on,
133
+ and narrow the value down over a series of ordinary-looking requests. Equal
134
+ values also sort together, which is enough to tell that two accounts share a
135
+ password hash.
136
+
137
+ So passing parameters straight through is safe as far as SQL injection goes,
138
+ but *which* columns may be sorted is an authorization question, and only your
139
+ application can answer it. Filter the parameters before they reach `#sort`:
140
+
141
+ ```ruby
142
+ SORTABLE = %w[name created_at].freeze
143
+
144
+ Property.sort(sort_params.slice(*SORTABLE))
145
+ ```
146
+
147
+ [StandardAPI](https://github.com/malomalo/standardapi) does this with an ACL,
148
+ resolving per request which attributes a user may read and sort by.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require 'active_record/relation'
3
5
 
@@ -8,6 +10,15 @@ module ActiveRecord
8
10
  # delegation tests assert against — while relations still respond to them.
9
11
  module Sort
10
12
 
13
+ # Raised when a sort references an unrecognized column or association,
14
+ # or an unknown direction. Subclasses StatementInvalid so existing
15
+ # `rescue ActiveRecord::StatementInvalid` handlers — the safety
16
+ # contract that makes it OK to pass request params straight through —
17
+ # keep working, while callers can rescue this narrower class to tell a
18
+ # bad sort parameter apart from a genuine database error.
19
+ class InvalidSort < ActiveRecord::StatementInvalid
20
+ end
21
+
11
22
  # ordering:
12
23
  # :id
13
24
  # :name, :id
@@ -29,106 +40,167 @@ module ActiveRecord
29
40
  ordering.flatten!
30
41
  return resource if ordering.size == 0
31
42
 
32
- order_columns = []
33
43
  ordering.each do |order|
34
44
  order = Array(order)
35
45
  order.each do |column_or_relation, options|
36
- if column_or_relation.to_sym == :random
46
+ key = column_or_relation.to_s
47
+
48
+ if key == 'random'
37
49
  resource = resource.order(Arel::Nodes::RandomOrdering.new)
38
- elsif self.column_names.include?(column_or_relation.to_s)
39
- resource = resource.sort_for_column(self.arel_table[column_or_relation.to_s], options)
40
- elsif reflect_on_association(column_or_relation.to_sym)
41
- resource = resource.sort_for_relation(column_or_relation.to_sym, options, order_columns)
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)
42
54
  else
43
- raise ActiveRecord::StatementInvalid.new("Unkown column #{column_or_relation}")
55
+ raise InvalidSort.new("Unknown column #{key}")
44
56
  end
45
57
  end
46
58
  end
47
-
48
- if order_columns.present?
49
- resource = resource.select(resource.klass.arel_table[Arel::Nodes::SqlLiteral.new('*')], *order_columns)
50
- end
51
59
 
52
60
  resource
53
61
  end
54
62
 
55
- # TODO: probably don't need to cast to sym
63
+ # Normalizes per-column sort options into [direction, nulls]. A blank
64
+ # direction — a bare column, or "" as query params often produce —
65
+ # means 'asc'; a blank or nil value means no NULLS clause.
66
+ def sort_direction_and_nulls(options)
67
+ if options.blank?
68
+ ['asc', nil]
69
+ elsif options.is_a?(Hash) || options.class.name == "ActionController::Parameters"
70
+ direction = options.keys.first.to_s.downcase
71
+
72
+ if direction == 'asc' || direction == 'desc'
73
+ [direction, sort_nulls(options.values.first)]
74
+ else
75
+ [direction, nil]
76
+ end
77
+ else
78
+ [options.to_s.downcase, nil]
79
+ end
80
+ end
81
+
82
+ # Cast on the way out: Arel's visitor matches the nulls value with
83
+ # `case o.nulls when :nulls_first`, so this is the one value that has
84
+ # to reach the node as a symbol.
85
+ def sort_nulls(value)
86
+ return nil if value.blank?
87
+
88
+ nulls = value.to_s.downcase
89
+ if nulls != 'nulls_first' && nulls != 'nulls_last'
90
+ raise InvalidSort.new("Unknown nulls ordering #{value}")
91
+ end
92
+
93
+ nulls.to_sym
94
+ end
95
+
56
96
  def sort_for_column(column, options)
57
- direction = (options.is_a?(Hash) || options.class.name == "ActionController::Parameters" ? options.keys.first.to_sym : options.to_s.downcase.to_sym)
97
+ direction, nulls = sort_direction_and_nulls(options)
58
98
 
59
- nulls = (options.is_a?(Hash) ? options.values.first.to_sym : nil)
60
- if direction == :desc
99
+ if direction == 'desc'
61
100
  self.order(Arel::Nodes::Descending.new(column, nulls))
62
- elsif direction == :asc || direction == :''
101
+ elsif direction == 'asc'
63
102
  self.order(Arel::Nodes::Ascending.new(column, nulls))
64
103
  else
65
- raise ActiveRecord::StatementInvalid.new("Unkown ordering #{direction}")
104
+ raise InvalidSort.new("Unknown ordering #{direction}")
66
105
  end
67
106
  end
68
107
 
69
- def sort_for_relation(relation, options, order_columns)
108
+ # The sort key lives only in the ORDER BY — nothing is added to the
109
+ # select list. That keeps the ORDER BY self-contained (so pluck/ids,
110
+ # which replace the select list, keep the sort), leaves the caller's
111
+ # select untouched, and means loaded records carry exactly their own
112
+ # attributes (a selected join column would overwrite a same-named
113
+ # attribute on the base table).
114
+ def sort_for_relation(relation, options)
70
115
  resource = self
71
116
  relation = reflect_on_association(relation)
117
+ options = [options] if !options.is_a?(Array)
118
+
119
+ # LEFT JOIN the association, group by this table's primary key — so
120
+ # rows don't fan / duplicate and records with an empty collection
121
+ # still appear — and order by an aggregate of the requested column:
122
+ #
123
+ # SELECT properties.*
124
+ # FROM properties
125
+ # LEFT JOIN properties_tags ON properties_tags.property_id = properties.id
126
+ # LEFT JOIN tags ON tags.id = properties_tags.tag_id
127
+ # GROUP BY properties.id
128
+ # ORDER BY MIN(tags.name) ASC
129
+ #
130
+ # Ascending keys each record by its smallest member (MIN), descending
131
+ # by its largest (MAX) — the member you'd expect to see first in that
132
+ # direction. Toggling asc/desc therefore re-keys multi-value records
133
+ # rather than strictly reversing the list.
134
+ options.each do |order|
135
+ Array(order).each do |column_name, column_options|
136
+ if !relation.klass.column_names.include?(column_name.to_s)
137
+ raise InvalidSort.new("Unknown column #{column_name}")
138
+ end
72
139
 
73
- if relation.macro == :has_many
74
- options = [options] if !options.is_a?(Array)
75
-
76
- options.each do |order|
77
- order = Array(order)
78
- order.each do |column, options|
79
- column = Arel::Attributes::Relation.new(relation.klass.arel_table[column], relation.name)
80
- order_columns.push(column)
81
- direction = (options.is_a?(Hash) ? options.keys.first.to_sym : options.to_s.downcase.to_sym)
82
-
83
- nulls = (options.is_a?(Hash) ? options.values.first.to_sym : nil)
84
- if direction == :desc
85
- # aggregation = Arel::Nodes::Max.new([column], "max_#{relation.name}_#{column.name}")
86
- # order = Arel::Nodes::Descending.new(Arel::Nodes::SqlLiteral.new("max_#{relation.name}_#{column.name}"), nulls)
87
-
88
- if relation.options[:through]
89
- resource = resource.joins(relation.options[:through] => relation.source_reflection_name)
90
- else
91
- resource = resource.joins(relation.name)
92
- end
93
- # resource = resource.select(aggregation)
94
- # resource = resource.order(order)
95
- resource = resource.order(Arel::Nodes::Descending.new(column, nulls))
96
- else
97
- # aggregation = Arel::Nodes::Min.new([column], "min_#{relation.name}_#{column.name}")
98
- order = Arel::Nodes::Ascending.new(Arel::Nodes::SqlLiteral.new("min_#{relation.name}_#{column.name}"), nulls)
99
-
100
- resource = resource.joins(relation.name)
101
- # resource = resource.select(aggregation)
102
- # resource = resource.order(order)
103
- resource = resource.order(Arel::Nodes::Ascending.new(column, nulls))
104
- end
140
+ # A collection (has_many / has_and_belongs_to_many) wraps the
141
+ # attribute in an Arel::Attributes::Relation so the aggregate
142
+ # references the joined table correctly; a singular association
143
+ # (belongs_to / has_one) uses the plain attribute.
144
+ column = if relation.collection?
145
+ Arel::Attributes::Relation.new(relation.klass.arel_table[column_name], relation.name)
146
+ else
147
+ relation.klass.arel_table[column_name]
105
148
  end
106
- end
107
- elsif relation.macro == :belongs_to || relation.macro == :has_one
108
- options = [options] if !options.is_a?(Array)
109
-
110
- options.each do |order|
111
- order = Array(order)
112
- order.each do |column, options|
113
- column = relation.klass.arel_table[column]
114
- order_columns.push(column)
115
- direction = (options.is_a?(Hash) ? options.keys.first.to_sym : options.to_s.downcase.to_sym)
116
-
117
- nulls = (options.is_a?(Hash) ? options.values.first.to_sym : nil)
118
- if direction == :asc
119
- order = Arel::Nodes::Ascending.new(column, nulls)
120
- else
121
- order = Arel::Nodes::Descending.new(column, nulls)
122
- end
123
-
124
- resource = resource.left_outer_joins(relation.name)
125
- resource = resource.order(order)
149
+
150
+ direction, nulls = sort_direction_and_nulls(column_options)
151
+
152
+ order = if direction == 'desc'
153
+ Arel::Nodes::Descending.new(column.maximum, nulls)
154
+ elsif direction == 'asc'
155
+ Arel::Nodes::Ascending.new(column.minimum, nulls)
156
+ else
157
+ raise InvalidSort.new("Unknown ordering #{direction}")
126
158
  end
159
+
160
+ resource = resource.left_outer_joins(relation.name)
161
+ resource = resource.order(order)
127
162
  end
128
163
  end
129
164
 
165
+ # Group by the primary key so the aggregate collapses the joined,
166
+ # fanned-out rows to one per record. Every relation sort adds this,
167
+ # so only add it once: ActiveRecord < 8.1's group! appends rather
168
+ # than unioning, which would leave a duplicate [pk, pk] that trips
169
+ # the count override's group_values check below.
170
+ # TODO: once Rails <= 8.0 is no longer supported, group! unions on
171
+ # its own — drop the include? guard and just group(primary_key).
172
+ primary_key = klass.arel_table[klass.primary_key]
173
+ resource = resource.group(primary_key) unless resource.group_values.include?(primary_key)
174
+ # Tag the relation for the count override below. Chained relations
175
+ # are built with clone, which copies instance variables, so the tag
176
+ # survives further chaining.
177
+ resource.instance_variable_set(:@sorted_by_relation, true)
130
178
  resource
131
179
  end
132
180
 
133
181
  end
182
+
183
+ module Sort
184
+ module Calculations
185
+ # sort_for_relation groups by the primary key and LEFT JOINs the
186
+ # relation, so aggregates would see grouped, fanned-out rows: count
187
+ # returns a per-group Hash, and sum/average weigh a record once per
188
+ # collection member. But a relation sort is order-only — records are
189
+ # never added or removed — so every aggregate has a well-defined
190
+ # answer: compute it over the base table restricted to the sorted
191
+ # relation's (distinct) primary keys. User joins and conditions still
192
+ # apply inside the subquery, and a user-added group falls through to
193
+ # the standard grouped behavior.
194
+ def calculate(operation, column_name)
195
+ if @sorted_by_relation && group_values == [klass.arel_table[klass.primary_key]]
196
+ klass.where(klass.primary_key => unscope(:group, :order, :select).select(klass.primary_key))
197
+ .calculate(operation, column_name)
198
+ else
199
+ super
200
+ end
201
+ end
202
+ end
203
+ end
134
204
  end
205
+
206
+ ActiveRecord::Relation.prepend(ActiveRecord::Sort::Calculations)
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module Sort
3
- VERSION = '7.0.0'
5
+ VERSION = '8.0.1'
4
6
  end
5
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require 'arel/extensions'
3
5
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-sort
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 8.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
@@ -103,18 +103,18 @@ dependencies:
103
103
  name: simplecov
104
104
  requirement: !ruby/object:Gem::Requirement
105
105
  requirements:
106
- - - ">="
106
+ - - '='
107
107
  - !ruby/object:Gem::Version
108
- version: '0'
108
+ version: 1.0.0.rc5
109
109
  type: :development
110
110
  prerelease: false
111
111
  version_requirements: !ruby/object:Gem::Requirement
112
112
  requirements:
113
- - - ">="
113
+ - - '='
114
114
  - !ruby/object:Gem::Version
115
- version: '0'
115
+ version: 1.0.0.rc5
116
116
  - !ruby/object:Gem::Dependency
117
- name: factory_bot
117
+ name: faker
118
118
  requirement: !ruby/object:Gem::Requirement
119
119
  requirements:
120
120
  - - ">="
@@ -128,35 +128,35 @@ dependencies:
128
128
  - !ruby/object:Gem::Version
129
129
  version: '0'
130
130
  - !ruby/object:Gem::Dependency
131
- name: faker
131
+ name: sunstone
132
132
  requirement: !ruby/object:Gem::Requirement
133
133
  requirements:
134
134
  - - ">="
135
135
  - !ruby/object:Gem::Version
136
- version: '0'
136
+ version: 7.0.0
137
137
  type: :development
138
138
  prerelease: false
139
139
  version_requirements: !ruby/object:Gem::Requirement
140
140
  requirements:
141
141
  - - ">="
142
142
  - !ruby/object:Gem::Version
143
- version: '0'
143
+ version: 7.0.0
144
144
  - !ruby/object:Gem::Dependency
145
- name: sunstone
145
+ name: webmock
146
146
  requirement: !ruby/object:Gem::Requirement
147
147
  requirements:
148
148
  - - ">="
149
149
  - !ruby/object:Gem::Version
150
- version: 7.0.0
150
+ version: '0'
151
151
  type: :development
152
152
  prerelease: false
153
153
  version_requirements: !ruby/object:Gem::Requirement
154
154
  requirements:
155
155
  - - ">="
156
156
  - !ruby/object:Gem::Version
157
- version: 7.0.0
157
+ version: '0'
158
158
  - !ruby/object:Gem::Dependency
159
- name: webmock
159
+ name: debug
160
160
  requirement: !ruby/object:Gem::Requirement
161
161
  requirements:
162
162
  - - ">="