activerecord-sort 8.0.1 → 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 +4 -4
- data/CHANGELOG.md +7 -71
- data/README.md +7 -82
- data/ext/active_record/base.rb +80 -113
- data/lib/active_record/sort/version.rb +1 -3
- data/lib/active_record/sort.rb +0 -3
- metadata +30 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55d032ac200c1f9c9bc9a06f988d54744427dcb2afbb2d0f82e89b7f7e6f32de
|
|
4
|
+
data.tar.gz: 5e10e73498fb36d17d579f9610a79d54ff254b3e36eb5940f9971e5538f15300
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91149455ebd6e9bf3f44b78a4e249c11335ca900923898dc4b90a10113b00276ab44dbe6412f58829f41aa40a04b3d664238e261633c8d3efcfee9087480d78f
|
|
7
|
+
data.tar.gz: 6e8028be2e9a156bd5a36ff0fc566121b2313aaa4aa48a51b48e41d344f7f59f10837f7c4cdee2fc8a98c763e8962b6fe9a406d560c77f482b26dc4c03651248
|
data/CHANGELOG.md
CHANGED
|
@@ -1,39 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 10.0.0.rc1 (July 15, 2026)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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.
|
|
37
9
|
|
|
38
10
|
### Breaking changes
|
|
39
11
|
|
|
@@ -66,19 +38,13 @@
|
|
|
66
38
|
`Address.sort(property: :name)`) now defaults to ascending, matching
|
|
67
39
|
every other sort form (previously descending).
|
|
68
40
|
- An unknown sort direction on a relation sort (e.g. `:dsc`) now raises
|
|
69
|
-
`ActiveRecord::
|
|
41
|
+
`ActiveRecord::StatementInvalid` (previously sorted ascending
|
|
70
42
|
silently), matching column sorts.
|
|
43
|
+
- ActiveRecord 7.1 or newer is required (previously 6.1).
|
|
71
44
|
|
|
72
45
|
### Added
|
|
73
46
|
|
|
74
47
|
- 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
48
|
- Sorts of different types compose: they can be combined in one call
|
|
83
49
|
(`Property.sort(:name, tags: :name, addresses: :id)`) or chained
|
|
84
50
|
(`.sort(...).sort(...)`), sharing a single `GROUP BY`.
|
|
@@ -100,33 +66,3 @@
|
|
|
100
66
|
there).
|
|
101
67
|
- Combining a `has_and_belongs_to_many` sort with another relation sort
|
|
102
68
|
raised `PG::GroupingError`.
|
|
103
|
-
|
|
104
|
-
## [7.0.0] - 2026-08-27
|
|
105
|
-
|
|
106
|
-
The gem's version is now independent of the Rails version it targets.
|
|
107
|
-
|
|
108
|
-
### Breaking Changes
|
|
109
|
-
|
|
110
|
-
- Removed the public `random_sort` method. Use `sort(:random)` instead.
|
|
111
|
-
|
|
112
|
-
### Changed
|
|
113
|
-
|
|
114
|
-
- Require `activerecord >= 8.0.0, < 9.0`; support for Rails 7.1 and 7.2 has
|
|
115
|
-
been dropped.
|
|
116
|
-
- Require `arel-extensions >= 9.0.0`.
|
|
117
|
-
- `#sort` called with no arguments now falls back to Ruby's `Enumerable#sort`
|
|
118
|
-
(loads the records and sorts them by `<=>`) instead of returning the
|
|
119
|
-
relation. Pass arguments to use the ordering DSL; `sort(nil)` and `sort([])`
|
|
120
|
-
still return the relation for chaining.
|
|
121
|
-
|
|
122
|
-
### Internal
|
|
123
|
-
|
|
124
|
-
- The `sort`, `sort_for_column`, and `sort_for_relation` methods are now
|
|
125
|
-
defined in an `ActiveRecord::Sort` module prepended onto
|
|
126
|
-
`ActiveRecord::QueryMethods`, rather than reopening `QueryMethods` directly.
|
|
127
|
-
Relations still respond to them, but they no longer appear in
|
|
128
|
-
`ActiveRecord::QueryMethods.public_instance_methods(false)` — which keeps
|
|
129
|
-
Rails' internal delegation invariants intact.
|
|
130
|
-
|
|
131
|
-
[8.0.0]: https://github.com/malomalo/activerecord-sort/releases/tag/v8.0.0
|
|
132
|
-
[7.0.0]: https://github.com/malomalo/activerecord-sort/releases/tag/v7.0.0
|
data/README.md
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
# ActiveRecord::Sort
|
|
2
2
|
|
|
3
|
-
`ActiveRecord::Sort` provides
|
|
4
|
-
query by it. Only recognized columns and associations produce SQL — anything
|
|
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).
|
|
11
|
-
|
|
12
|
-
Requirements
|
|
13
|
-
------------
|
|
14
|
-
|
|
15
|
-
- Rails / ActiveRecord >= 8.0
|
|
3
|
+
`ActiveRecord::Sort` provides and easy way to accept user input and order a query by the input.
|
|
16
4
|
|
|
17
5
|
Installation
|
|
18
6
|
------------
|
|
19
7
|
|
|
20
|
-
Add `
|
|
8
|
+
Add `sunstone` to your Gemfile and run `bundle`:
|
|
21
9
|
|
|
22
10
|
```ruby
|
|
23
11
|
gem 'activerecord-sort', require: 'active_record/sort'
|
|
@@ -27,10 +15,8 @@ Or install the gem and require it:
|
|
|
27
15
|
|
|
28
16
|
```sh
|
|
29
17
|
gem install activerecord-sort
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```ruby
|
|
33
|
-
require 'active_record/sort'
|
|
18
|
+
irb
|
|
19
|
+
# => require('active_record/sort')
|
|
34
20
|
```
|
|
35
21
|
|
|
36
22
|
Examples
|
|
@@ -47,10 +33,10 @@ Property.sort(:id, :name).to_sql
|
|
|
47
33
|
Property.sort(id: :desc).to_sql
|
|
48
34
|
# => "...ORDER BY properties.id DESC"
|
|
49
35
|
|
|
50
|
-
Property.sort(id: {asc: :nulls_first})
|
|
36
|
+
Property.sort(id: {asc: :nulls_first})
|
|
51
37
|
# => "...ORDER BY properties.id ASC NULLS FIRST"
|
|
52
38
|
|
|
53
|
-
Property.sort(id: {asc: :nulls_last})
|
|
39
|
+
Property.sort(id: {asc: :nulls_last})
|
|
54
40
|
# => "...ORDER BY properties.id ASC NULLS LAST"
|
|
55
41
|
```
|
|
56
42
|
|
|
@@ -58,10 +44,7 @@ It can also sort on relations. A relation sort groups by the sorted table's
|
|
|
58
44
|
primary key — so each record appears once and records with no associated
|
|
59
45
|
rows are still included — and orders by an aggregate of the requested
|
|
60
46
|
column: `MIN` ascending or `MAX` descending, keying each record by the
|
|
61
|
-
member you'd expect to see first in that direction
|
|
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:
|
|
47
|
+
member you'd expect to see first in that direction:
|
|
65
48
|
|
|
66
49
|
```ruby
|
|
67
50
|
Property.sort(addresses: :id).to_sql
|
|
@@ -88,61 +71,3 @@ A relation sort is order-only — it never adds or removes records — so
|
|
|
88
71
|
aggregates on a sorted relation (`count`, `sum`, `average`, `minimum`,
|
|
89
72
|
`maximum`) are computed over the records themselves, not the sort's
|
|
90
73
|
grouped and joined rows.
|
|
91
|
-
|
|
92
|
-
Order randomly:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
Property.sort(:random).to_sql
|
|
96
|
-
# => "...ORDER BY RANDOM()"
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Unrecognized columns raise, so unfiltered params can't inject SQL:
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
Property.sort(:name_or_something_unexpected)
|
|
103
|
-
# => raises ActiveRecord::Sort::InvalidSort
|
|
104
|
-
```
|
|
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
|
-
|
|
110
|
-
Called with no arguments, `#sort` behaves like Ruby's `Enumerable#sort` —
|
|
111
|
-
it loads the records and sorts them by `<=>` — rather than building a query:
|
|
112
|
-
|
|
113
|
-
```ruby
|
|
114
|
-
Property.all.sort # => Array of Property, sorted by <=>
|
|
115
|
-
Property.sort(nil) # => relation (unchanged), for chaining
|
|
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.
|
data/ext/active_record/base.rb
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require 'active_record'
|
|
4
2
|
require 'active_record/relation'
|
|
5
3
|
|
|
6
4
|
module ActiveRecord
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
# QueryMethods.public_instance_methods(false) — which Rails' own
|
|
10
|
-
# delegation tests assert against — while relations still respond to them.
|
|
11
|
-
module Sort
|
|
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
|
|
5
|
+
module QueryMethods
|
|
6
|
+
# class << self
|
|
21
7
|
|
|
22
8
|
# ordering:
|
|
23
9
|
# :id
|
|
@@ -27,14 +13,7 @@ module ActiveRecord
|
|
|
27
13
|
# :listings => :id
|
|
28
14
|
# :listings => {:id => {:asc => :nulls_first}}
|
|
29
15
|
# :random
|
|
30
|
-
def sort(*ordering
|
|
31
|
-
# With no arguments, fall back to Ruby's Enumerable#sort so code (and
|
|
32
|
-
# Rails' own test suite) that calls `relation.sort` expecting Ruby
|
|
33
|
-
# semantics keeps working. The ordering DSL only applies when given
|
|
34
|
-
# arguments; `sort(nil)`/`sort([])` still return the relation for
|
|
35
|
-
# chaining.
|
|
36
|
-
return super(&block) if ordering.empty?
|
|
37
|
-
|
|
16
|
+
def sort(*ordering)
|
|
38
17
|
resource = all
|
|
39
18
|
ordering.compact!
|
|
40
19
|
ordering.flatten!
|
|
@@ -43,65 +22,48 @@ module ActiveRecord
|
|
|
43
22
|
ordering.each do |order|
|
|
44
23
|
order = Array(order)
|
|
45
24
|
order.each do |column_or_relation, options|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
resource = resource.
|
|
50
|
-
elsif
|
|
51
|
-
resource = resource.
|
|
52
|
-
elsif reflect_on_association(key)
|
|
53
|
-
resource = resource.sort_for_relation(key, options)
|
|
25
|
+
if column_or_relation.to_sym == :random
|
|
26
|
+
resource = resource.random_sort
|
|
27
|
+
elsif self.column_names.include?(column_or_relation.to_s)
|
|
28
|
+
resource = resource.sort_for_column(self.arel_table[column_or_relation.to_s], options)
|
|
29
|
+
elsif reflect_on_association(column_or_relation.to_sym)
|
|
30
|
+
resource = resource.sort_for_relation(column_or_relation.to_sym, options)
|
|
54
31
|
else
|
|
55
|
-
raise
|
|
32
|
+
raise ActiveRecord::StatementInvalid.new("Unkown column #{column_or_relation}")
|
|
56
33
|
end
|
|
57
34
|
end
|
|
58
35
|
end
|
|
59
36
|
|
|
60
37
|
resource
|
|
61
38
|
end
|
|
39
|
+
|
|
40
|
+
def random_sort
|
|
41
|
+
self.order(Arel::Nodes::RandomOrdering.new)
|
|
42
|
+
end
|
|
62
43
|
|
|
63
44
|
# Normalizes per-column sort options into [direction, nulls]. A blank
|
|
64
45
|
# direction — a bare column, or "" as query params often produce —
|
|
65
|
-
# means
|
|
46
|
+
# means :asc.
|
|
47
|
+
# TODO: probably don't need to cast to sym
|
|
66
48
|
def sort_direction_and_nulls(options)
|
|
67
|
-
if options.
|
|
68
|
-
[
|
|
69
|
-
elsif options.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if direction == 'asc' || direction == 'desc'
|
|
73
|
-
[direction, sort_nulls(options.values.first)]
|
|
74
|
-
else
|
|
75
|
-
[direction, nil]
|
|
76
|
-
end
|
|
49
|
+
if options.is_a?(Hash) || options.class.name == "ActionController::Parameters"
|
|
50
|
+
[options.keys.first.to_sym, options.values.first.to_sym]
|
|
51
|
+
elsif options.blank?
|
|
52
|
+
[:asc, nil]
|
|
77
53
|
else
|
|
78
|
-
[options.to_s.downcase, nil]
|
|
54
|
+
[options.to_s.downcase.to_sym, nil]
|
|
79
55
|
end
|
|
80
56
|
end
|
|
81
57
|
|
|
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
|
-
|
|
96
58
|
def sort_for_column(column, options)
|
|
97
59
|
direction, nulls = sort_direction_and_nulls(options)
|
|
98
60
|
|
|
99
|
-
if direction ==
|
|
61
|
+
if direction == :desc
|
|
100
62
|
self.order(Arel::Nodes::Descending.new(column, nulls))
|
|
101
|
-
elsif direction ==
|
|
63
|
+
elsif direction == :asc
|
|
102
64
|
self.order(Arel::Nodes::Ascending.new(column, nulls))
|
|
103
65
|
else
|
|
104
|
-
raise
|
|
66
|
+
raise ActiveRecord::StatementInvalid.new("Unkown ordering #{direction}")
|
|
105
67
|
end
|
|
106
68
|
end
|
|
107
69
|
|
|
@@ -114,63 +76,68 @@ module ActiveRecord
|
|
|
114
76
|
def sort_for_relation(relation, options)
|
|
115
77
|
resource = self
|
|
116
78
|
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
|
|
139
79
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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)
|
|
148
115
|
end
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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)
|
|
158
136
|
end
|
|
159
|
-
|
|
160
|
-
resource = resource.left_outer_joins(relation.name)
|
|
161
|
-
resource = resource.order(order)
|
|
162
137
|
end
|
|
163
138
|
end
|
|
164
139
|
|
|
165
|
-
|
|
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)
|
|
140
|
+
resource = resource.group(klass.arel_table[klass.primary_key])
|
|
174
141
|
# Tag the relation for the count override below. Chained relations
|
|
175
142
|
# are built with clone, which copies instance variables, so the tag
|
|
176
143
|
# survives further chaining.
|
data/lib/active_record/sort.rb
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require 'active_record'
|
|
4
2
|
require 'arel/extensions'
|
|
5
3
|
|
|
6
4
|
require File.expand_path(File.join(__FILE__, '../../../ext/active_record/base'))
|
|
7
5
|
|
|
8
|
-
ActiveRecord::QueryMethods.prepend(ActiveRecord::Sort)
|
|
9
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
173
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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.
|
|
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
|