activerecord-sort 8.0.0 → 10.0.0.rc1

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: 50d689663cb6717dab8e034f88d8645cf24a31489e322ca79d9cd52634281dc4
4
- data.tar.gz: 544297bd2fc5b8322f8a719d3a4f52e235681a1c756dc191ac74fd69a4c556a7
3
+ metadata.gz: 55d032ac200c1f9c9bc9a06f988d54744427dcb2afbb2d0f82e89b7f7e6f32de
4
+ data.tar.gz: 5e10e73498fb36d17d579f9610a79d54ff254b3e36eb5940f9971e5538f15300
5
5
  SHA512:
6
- metadata.gz: b0caa63a30b528ed30f2fa2ff5c29fe779fd633e9d30ec4404ede2c8b08f9a24a1832b3bd4bd10c39914bebe07cfd8fe23b10ce93496b254d6bb81d2450a3c9e
7
- data.tar.gz: afc2f2911285ed3e681a39013013ad1b9ffbce9161a4ac5bb2da6fda40ddd03421942f3b42eaf0c0a3865878fe667759d4c77df0b8962b431937267be40a9d70
6
+ metadata.gz: 91149455ebd6e9bf3f44b78a4e249c11335ca900923898dc4b90a10113b00276ab44dbe6412f58829f41aa40a04b3d664238e261633c8d3efcfee9087480d78f
7
+ data.tar.gz: 6e8028be2e9a156bd5a36ff0fc566121b2313aaa4aa48a51b48e41d344f7f59f10837f7c4cdee2fc8a98c763e8962b6fe9a406d560c77f482b26dc4c03651248
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## [8.0.0] - 2026-08-27
3
+ ## 10.0.0.rc1 (July 15, 2026)
4
+
5
+ Starting with this release the gem is versioned independently of Rails,
6
+ following [semantic versioning](https://semver.org). Earlier releases
7
+ (6.x) tracked the minimum supported Rails version; the jump to 10 makes
8
+ the break explicit so the version can't be misread as a Rails version.
4
9
 
5
10
  ### Breaking changes
6
11
 
@@ -33,19 +38,13 @@
33
38
  `Address.sort(property: :name)`) now defaults to ascending, matching
34
39
  every other sort form (previously descending).
35
40
  - An unknown sort direction on a relation sort (e.g. `:dsc`) now raises
36
- `ActiveRecord::Sort::InvalidSort` (previously sorted ascending
41
+ `ActiveRecord::StatementInvalid` (previously sorted ascending
37
42
  silently), matching column sorts.
43
+ - ActiveRecord 7.1 or newer is required (previously 6.1).
38
44
 
39
45
  ### Added
40
46
 
41
47
  - Sorting by `has_and_belongs_to_many` relations.
42
- - Unrecognized sort columns, associations, and directions raise
43
- `ActiveRecord::Sort::InvalidSort`, a subclass of
44
- `ActiveRecord::StatementInvalid` — so existing
45
- `rescue ActiveRecord::StatementInvalid` handlers still catch bad sort
46
- parameters, while callers can rescue the narrower class. Relation sort
47
- columns are now validated up front (against the associated model's
48
- columns) instead of only failing once the query reaches the database.
49
48
  - Sorts of different types compose: they can be combined in one call
50
49
  (`Property.sort(:name, tags: :name, addresses: :id)`) or chained
51
50
  (`.sort(...).sort(...)`), sharing a single `GROUP BY`.
@@ -67,33 +66,3 @@
67
66
  there).
68
67
  - Combining a `has_and_belongs_to_many` sort with another relation sort
69
68
  raised `PG::GroupingError`.
70
-
71
- ## [7.0.0] - 2026-08-27
72
-
73
- The gem's version is now independent of the Rails version it targets.
74
-
75
- ### Breaking Changes
76
-
77
- - Removed the public `random_sort` method. Use `sort(:random)` instead.
78
-
79
- ### Changed
80
-
81
- - Require `activerecord >= 8.0.0, < 9.0`; support for Rails 7.1 and 7.2 has
82
- been dropped.
83
- - Require `arel-extensions >= 9.0.0`.
84
- - `#sort` called with no arguments now falls back to Ruby's `Enumerable#sort`
85
- (loads the records and sorts them by `<=>`) instead of returning the
86
- relation. Pass arguments to use the ordering DSL; `sort(nil)` and `sort([])`
87
- still return the relation for chaining.
88
-
89
- ### Internal
90
-
91
- - The `sort`, `sort_for_column`, and `sort_for_relation` methods are now
92
- defined in an `ActiveRecord::Sort` module prepended onto
93
- `ActiveRecord::QueryMethods`, rather than reopening `QueryMethods` directly.
94
- Relations still respond to them, but they no longer appear in
95
- `ActiveRecord::QueryMethods.public_instance_methods(false)` — which keeps
96
- Rails' internal delegation invariants intact.
97
-
98
- [8.0.0]: https://github.com/malomalo/activerecord-sort/releases/tag/v8.0.0
99
- [7.0.0]: https://github.com/malomalo/activerecord-sort/releases/tag/v7.0.0
data/README.md CHANGED
@@ -1,19 +1,11 @@
1
1
  # ActiveRecord::Sort
2
2
 
3
- `ActiveRecord::Sort` provides an easy, safe way to accept user input and order a
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.
7
-
8
- Requirements
9
- ------------
10
-
11
- - Rails / ActiveRecord >= 8.0
3
+ `ActiveRecord::Sort` provides and easy way to accept user input and order a query by the input.
12
4
 
13
5
  Installation
14
6
  ------------
15
7
 
16
- Add `activerecord-sort` to your Gemfile and run `bundle`:
8
+ Add `sunstone` to your Gemfile and run `bundle`:
17
9
 
18
10
  ```ruby
19
11
  gem 'activerecord-sort', require: 'active_record/sort'
@@ -23,10 +15,8 @@ Or install the gem and require it:
23
15
 
24
16
  ```sh
25
17
  gem install activerecord-sort
26
- ```
27
-
28
- ```ruby
29
- require 'active_record/sort'
18
+ irb
19
+ # => require('active_record/sort')
30
20
  ```
31
21
 
32
22
  Examples
@@ -43,10 +33,10 @@ Property.sort(:id, :name).to_sql
43
33
  Property.sort(id: :desc).to_sql
44
34
  # => "...ORDER BY properties.id DESC"
45
35
 
46
- Property.sort(id: {asc: :nulls_first}).to_sql
36
+ Property.sort(id: {asc: :nulls_first})
47
37
  # => "...ORDER BY properties.id ASC NULLS FIRST"
48
38
 
49
- Property.sort(id: {asc: :nulls_last}).to_sql
39
+ Property.sort(id: {asc: :nulls_last})
50
40
  # => "...ORDER BY properties.id ASC NULLS LAST"
51
41
  ```
52
42
 
@@ -54,10 +44,7 @@ It can also sort on relations. A relation sort groups by the sorted table's
54
44
  primary key — so each record appears once and records with no associated
55
45
  rows are still included — and orders by an aggregate of the requested
56
46
  column: `MIN` ascending or `MAX` descending, keying each record by the
57
- member you'd expect to see first in that direction. A record with no
58
- associated rows has a `NULL` sort key; where `NULL`s land is
59
- database-dependent, so pass `nulls_first`/`nulls_last` to place those
60
- records explicitly:
47
+ member you'd expect to see first in that direction:
61
48
 
62
49
  ```ruby
63
50
  Property.sort(addresses: :id).to_sql
@@ -84,29 +71,3 @@ A relation sort is order-only — it never adds or removes records — so
84
71
  aggregates on a sorted relation (`count`, `sum`, `average`, `minimum`,
85
72
  `maximum`) are computed over the records themselves, not the sort's
86
73
  grouped and joined rows.
87
-
88
- Order randomly:
89
-
90
- ```ruby
91
- Property.sort(:random).to_sql
92
- # => "...ORDER BY RANDOM()"
93
- ```
94
-
95
- Unrecognized columns raise, so unfiltered params can't inject SQL:
96
-
97
- ```ruby
98
- Property.sort(:name_or_something_unexpected)
99
- # => raises ActiveRecord::Sort::InvalidSort
100
- ```
101
-
102
- `ActiveRecord::Sort::InvalidSort` subclasses `ActiveRecord::StatementInvalid`,
103
- so existing `rescue ActiveRecord::StatementInvalid` handlers keep catching bad
104
- sort parameters, while callers that want to can rescue the narrower class.
105
-
106
- Called with no arguments, `#sort` behaves like Ruby's `Enumerable#sort` —
107
- it loads the records and sorts them by `<=>` — rather than building a query:
108
-
109
- ```ruby
110
- Property.all.sort # => Array of Property, sorted by <=>
111
- Property.sort(nil) # => relation (unchanged), for chaining
112
- ```
@@ -2,20 +2,8 @@ require 'active_record'
2
2
  require 'active_record/relation'
3
3
 
4
4
  module ActiveRecord
5
- # Prepended onto ActiveRecord::QueryMethods (see lib/active_record/sort.rb).
6
- # Living in a separate module keeps these methods out of
7
- # QueryMethods.public_instance_methods(false) — which Rails' own
8
- # delegation tests assert against — while relations still respond to them.
9
- module Sort
10
-
11
- # Raised when a sort references an unrecognized column or association,
12
- # or an unknown direction. Subclasses StatementInvalid so existing
13
- # `rescue ActiveRecord::StatementInvalid` handlers — the safety
14
- # contract that makes it OK to pass request params straight through —
15
- # keep working, while callers can rescue this narrower class to tell a
16
- # bad sort parameter apart from a genuine database error.
17
- class InvalidSort < ActiveRecord::StatementInvalid
18
- end
5
+ module QueryMethods
6
+ # class << self
19
7
 
20
8
  # ordering:
21
9
  # :id
@@ -25,14 +13,7 @@ module ActiveRecord
25
13
  # :listings => :id
26
14
  # :listings => {:id => {:asc => :nulls_first}}
27
15
  # :random
28
- def sort(*ordering, &block)
29
- # With no arguments, fall back to Ruby's Enumerable#sort so code (and
30
- # Rails' own test suite) that calls `relation.sort` expecting Ruby
31
- # semantics keeps working. The ordering DSL only applies when given
32
- # arguments; `sort(nil)`/`sort([])` still return the relation for
33
- # chaining.
34
- return super(&block) if ordering.empty?
35
-
16
+ def sort(*ordering)
36
17
  resource = all
37
18
  ordering.compact!
38
19
  ordering.flatten!
@@ -42,19 +23,23 @@ module ActiveRecord
42
23
  order = Array(order)
43
24
  order.each do |column_or_relation, options|
44
25
  if column_or_relation.to_sym == :random
45
- resource = resource.order(Arel::Nodes::RandomOrdering.new)
26
+ resource = resource.random_sort
46
27
  elsif self.column_names.include?(column_or_relation.to_s)
47
28
  resource = resource.sort_for_column(self.arel_table[column_or_relation.to_s], options)
48
29
  elsif reflect_on_association(column_or_relation.to_sym)
49
30
  resource = resource.sort_for_relation(column_or_relation.to_sym, options)
50
31
  else
51
- raise InvalidSort.new("Unknown column #{column_or_relation}")
32
+ raise ActiveRecord::StatementInvalid.new("Unkown column #{column_or_relation}")
52
33
  end
53
34
  end
54
35
  end
55
36
 
56
37
  resource
57
38
  end
39
+
40
+ def random_sort
41
+ self.order(Arel::Nodes::RandomOrdering.new)
42
+ end
58
43
 
59
44
  # Normalizes per-column sort options into [direction, nulls]. A blank
60
45
  # direction — a bare column, or "" as query params often produce —
@@ -78,7 +63,7 @@ module ActiveRecord
78
63
  elsif direction == :asc
79
64
  self.order(Arel::Nodes::Ascending.new(column, nulls))
80
65
  else
81
- raise InvalidSort.new("Unknown ordering #{direction}")
66
+ raise ActiveRecord::StatementInvalid.new("Unkown ordering #{direction}")
82
67
  end
83
68
  end
84
69
 
@@ -91,63 +76,68 @@ module ActiveRecord
91
76
  def sort_for_relation(relation, options)
92
77
  resource = self
93
78
  relation = reflect_on_association(relation)
94
- options = [options] if !options.is_a?(Array)
95
-
96
- # LEFT JOIN the association, group by this table's primary key — so
97
- # rows don't fan / duplicate and records with an empty collection
98
- # still appear — and order by an aggregate of the requested column:
99
- #
100
- # SELECT properties.*
101
- # FROM properties
102
- # LEFT JOIN properties_tags ON properties_tags.property_id = properties.id
103
- # LEFT JOIN tags ON tags.id = properties_tags.tag_id
104
- # GROUP BY properties.id
105
- # ORDER BY MIN(tags.name) ASC
106
- #
107
- # Ascending keys each record by its smallest member (MIN), descending
108
- # by its largest (MAX) — the member you'd expect to see first in that
109
- # direction. Toggling asc/desc therefore re-keys multi-value records
110
- # rather than strictly reversing the list.
111
- options.each do |order|
112
- Array(order).each do |column_name, column_options|
113
- if !relation.klass.column_names.include?(column_name.to_s)
114
- raise InvalidSort.new("Unknown column #{column_name}")
115
- end
116
79
 
117
- # A collection (has_many / has_and_belongs_to_many) wraps the
118
- # attribute in an Arel::Attributes::Relation so the aggregate
119
- # references the joined table correctly; a singular association
120
- # (belongs_to / has_one) uses the plain attribute.
121
- column = if relation.collection?
122
- Arel::Attributes::Relation.new(relation.klass.arel_table[column_name], relation.name)
123
- else
124
- relation.klass.arel_table[column_name]
80
+ if relation.macro == :has_many || relation.macro == :has_and_belongs_to_many
81
+ options = [options] if !options.is_a?(Array)
82
+
83
+ options.each do |order|
84
+ Array(order).each do |column_name, options|
85
+ # LEFT JOIN the collection, group by this table's primary key —
86
+ # so rows don't fan / duplicate and records with an empty
87
+ # collection still appear — and order by an aggregate of the
88
+ # requested column:
89
+ #
90
+ # SELECT properties.*
91
+ # FROM properties
92
+ # LEFT JOIN properties_tags ON properties_tags.property_id = properties.id
93
+ # LEFT JOIN tags ON tags.id = properties_tags.tag_id
94
+ # GROUP BY properties.id
95
+ # ORDER BY MIN(tags.name) ASC
96
+ #
97
+ # Ascending keys each record by its smallest member (MIN),
98
+ # descending by its largest (MAX) — the member you'd expect to
99
+ # see first in that direction. Toggling asc/desc therefore
100
+ # re-keys multi-value records rather than strictly reversing
101
+ # the list.
102
+ column = Arel::Attributes::Relation.new(relation.klass.arel_table[column_name], relation.name)
103
+ direction, nulls = sort_direction_and_nulls(options)
104
+
105
+ order = if direction == :desc
106
+ Arel::Nodes::Descending.new(column.maximum, nulls)
107
+ elsif direction == :asc
108
+ Arel::Nodes::Ascending.new(column.minimum, nulls)
109
+ else
110
+ raise ActiveRecord::StatementInvalid.new("Unkown ordering #{direction}")
111
+ end
112
+
113
+ resource = resource.left_outer_joins(relation.name)
114
+ resource = resource.order(order)
125
115
  end
126
-
127
- direction, nulls = sort_direction_and_nulls(column_options)
128
-
129
- order = if direction == :desc
130
- Arel::Nodes::Descending.new(column.maximum, nulls)
131
- elsif direction == :asc
132
- Arel::Nodes::Ascending.new(column.minimum, nulls)
133
- else
134
- raise InvalidSort.new("Unknown ordering #{direction}")
116
+ end
117
+ elsif relation.macro == :belongs_to || relation.macro == :has_one
118
+ options = [options] if !options.is_a?(Array)
119
+
120
+ options.each do |order|
121
+ order = Array(order)
122
+ order.each do |column, options|
123
+ column = relation.klass.arel_table[column]
124
+ direction, nulls = sort_direction_and_nulls(options)
125
+
126
+ if direction == :desc
127
+ order = Arel::Nodes::Descending.new(column.maximum, nulls)
128
+ elsif direction == :asc
129
+ order = Arel::Nodes::Ascending.new(column.minimum, nulls)
130
+ else
131
+ raise ActiveRecord::StatementInvalid.new("Unkown ordering #{direction}")
132
+ end
133
+
134
+ resource = resource.left_outer_joins(relation.name)
135
+ resource = resource.order(order)
135
136
  end
136
-
137
- resource = resource.left_outer_joins(relation.name)
138
- resource = resource.order(order)
139
137
  end
140
138
  end
141
139
 
142
- # Group by the primary key so the aggregate collapses the joined,
143
- # fanned-out rows to one per record. Every relation sort adds this,
144
- # so only add it once: ActiveRecord < 8.1's group! appends rather
145
- # than unioning, which would leave a duplicate [pk, pk] that trips
146
- # the count override's group_values check below.
147
- # TODO: once Rails <= 8.0 is no longer supported, group! unions on
148
- # its own — drop the include? guard and just group(primary_key).
149
- primary_key = klass.arel_table[klass.primary_key]
150
- resource = resource.group(primary_key) unless resource.group_values.include?(primary_key)
140
+ resource = resource.group(klass.arel_table[klass.primary_key])
151
141
  # Tag the relation for the count override below. Chained relations
152
142
  # are built with clone, which copies instance variables, so the tag
153
143
  # survives further chaining.
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Sort
3
- VERSION = '8.0.0'
3
+ VERSION = '10.0.0.rc1'
4
4
  end
5
5
  end
@@ -3,5 +3,4 @@ require 'arel/extensions'
3
3
 
4
4
  require File.expand_path(File.join(__FILE__, '../../../ext/active_record/base'))
5
5
 
6
- ActiveRecord::QueryMethods.prepend(ActiveRecord::Sort)
7
6
  ActiveRecord::Querying.delegate :sort, to: :all
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: 8.0.0
4
+ version: 10.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
@@ -15,34 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 8.0.0
19
- - - "<"
20
- - !ruby/object:Gem::Version
21
- version: '9.0'
18
+ version: '7.1'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
23
  - - ">="
27
24
  - !ruby/object:Gem::Version
28
- version: 8.0.0
29
- - - "<"
30
- - !ruby/object:Gem::Version
31
- version: '9.0'
25
+ version: '7.1'
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: arel-extensions
34
28
  requirement: !ruby/object:Gem::Requirement
35
29
  requirements:
36
30
  - - ">="
37
31
  - !ruby/object:Gem::Version
38
- version: 9.0.0
32
+ version: 6.1.0
39
33
  type: :runtime
40
34
  prerelease: false
41
35
  version_requirements: !ruby/object:Gem::Requirement
42
36
  requirements:
43
37
  - - ">="
44
38
  - !ruby/object:Gem::Version
45
- version: 9.0.0
39
+ version: 6.1.0
46
40
  - !ruby/object:Gem::Dependency
47
41
  name: pg
48
42
  requirement: !ruby/object:Gem::Requirement
@@ -57,6 +51,20 @@ dependencies:
57
51
  - - ">="
58
52
  - !ruby/object:Gem::Version
59
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bundler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
60
68
  - !ruby/object:Gem::Dependency
61
69
  name: rake
62
70
  requirement: !ruby/object:Gem::Requirement
@@ -133,14 +141,14 @@ dependencies:
133
141
  requirements:
134
142
  - - ">="
135
143
  - !ruby/object:Gem::Version
136
- version: 7.0.0
144
+ version: 6.1.0.2
137
145
  type: :development
138
146
  prerelease: false
139
147
  version_requirements: !ruby/object:Gem::Requirement
140
148
  requirements:
141
149
  - - ">="
142
150
  - !ruby/object:Gem::Version
143
- version: 7.0.0
151
+ version: 6.1.0.2
144
152
  - !ruby/object:Gem::Dependency
145
153
  name: webmock
146
154
  requirement: !ruby/object:Gem::Requirement
@@ -169,14 +177,14 @@ dependencies:
169
177
  - - ">="
170
178
  - !ruby/object:Gem::Version
171
179
  version: '0'
172
- description: 'Adds a #sort query method to ActiveRecord that turns whitelisted user-supplied
173
- parameters into ORDER BY clauses -- ordering by columns or associations, ascending/descending,
174
- with NULLS handling and random ordering -- without exposing you to SQL injection.'
180
+ description: A safe way to accept user parameters and order against your ActiveRecord
181
+ Models
175
182
  email:
176
183
  - jonbracy@gmail.com
177
184
  executables: []
178
185
  extensions: []
179
- extra_rdoc_files: []
186
+ extra_rdoc_files:
187
+ - README.md
180
188
  files:
181
189
  - CHANGELOG.md
182
190
  - LICENSE
@@ -187,11 +195,10 @@ files:
187
195
  homepage: https://github.com/malomalo/activerecord-sort
188
196
  licenses:
189
197
  - MIT
190
- metadata:
191
- source_code_uri: https://github.com/malomalo/activerecord-sort
192
- changelog_uri: https://github.com/malomalo/activerecord-sort/blob/master/CHANGELOG.md
193
- rubygems_mfa_required: 'true'
194
- rdoc_options: []
198
+ metadata: {}
199
+ rdoc_options:
200
+ - "--main"
201
+ - README.md
195
202
  require_paths:
196
203
  - lib
197
204
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -205,7 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
212
  - !ruby/object:Gem::Version
206
213
  version: '0'
207
214
  requirements: []
208
- rubygems_version: 4.0.11
215
+ rubygems_version: 4.0.2
209
216
  specification_version: 4
210
217
  summary: A safe way to accept user parameters and order against your ActiveRecord
211
218
  Models