activerecord-sort 6.1.0.2 → 8.0.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 +99 -0
- data/README.md +69 -14
- data/ext/active_record/base.rb +183 -0
- data/lib/active_record/sort/version.rb +1 -1
- data/lib/active_record/sort.rb +1 -120
- metadata +37 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50d689663cb6717dab8e034f88d8645cf24a31489e322ca79d9cd52634281dc4
|
|
4
|
+
data.tar.gz: 544297bd2fc5b8322f8a719d3a4f52e235681a1c756dc191ac74fd69a4c556a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0caa63a30b528ed30f2fa2ff5c29fe779fd633e9d30ec4404ede2c8b08f9a24a1832b3bd4bd10c39914bebe07cfd8fe23b10ce93496b254d6bb81d2450a3c9e
|
|
7
|
+
data.tar.gz: afc2f2911285ed3e681a39013013ad1b9ffbce9161a4ac5bb2da6fda40ddd03421942f3b42eaf0c0a3865878fe667759d4c77df0b8962b431937267be40a9d70
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [8.0.0] - 2026-08-27
|
|
4
|
+
|
|
5
|
+
### Breaking changes
|
|
6
|
+
|
|
7
|
+
- Relation sorts are unified across association types. Sorting by any
|
|
8
|
+
relation (`has_many`, `has_and_belongs_to_many`, `has_one`,
|
|
9
|
+
`belongs_to`) now `LEFT OUTER JOIN`s the association, groups by the
|
|
10
|
+
sorted table's primary key, and orders by an aggregate of the requested
|
|
11
|
+
column. For `has_many` sorts this changes behavior:
|
|
12
|
+
- each record is returned once, instead of once per associated row
|
|
13
|
+
(the join no longer fans out into duplicates)
|
|
14
|
+
- records with no associated rows are included (previously dropped by
|
|
15
|
+
the `INNER JOIN`)
|
|
16
|
+
- Descending relation sorts key each record by its largest member
|
|
17
|
+
(`MAX`), ascending by its smallest (`MIN`) — the member you'd expect
|
|
18
|
+
to see first in that direction. For records with multiple associated
|
|
19
|
+
rows, descending is therefore not the reverse of ascending: a record
|
|
20
|
+
holding both extremes sorts first in both directions.
|
|
21
|
+
- Sort columns are no longer added to the `SELECT`:
|
|
22
|
+
- loaded records keep their own attributes — a joined sort column can
|
|
23
|
+
no longer overwrite a same-named attribute on the base record
|
|
24
|
+
(previously even `id` could be clobbered)
|
|
25
|
+
- `pluck` and `ids` keep the sort (previously raised on
|
|
26
|
+
`has_and_belongs_to_many` sorts)
|
|
27
|
+
- a caller's `select` is left untouched
|
|
28
|
+
- chaining `.distinct` after a relation sort now raises: PostgreSQL
|
|
29
|
+
requires `ORDER BY` expressions to appear in the select list for
|
|
30
|
+
`SELECT DISTINCT`. It previously appeared to work while silently
|
|
31
|
+
deduplicating over the wrong tuple.
|
|
32
|
+
- A bare `belongs_to`/`has_one` sort with no direction (e.g.
|
|
33
|
+
`Address.sort(property: :name)`) now defaults to ascending, matching
|
|
34
|
+
every other sort form (previously descending).
|
|
35
|
+
- An unknown sort direction on a relation sort (e.g. `:dsc`) now raises
|
|
36
|
+
`ActiveRecord::Sort::InvalidSort` (previously sorted ascending
|
|
37
|
+
silently), matching column sorts.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- 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
|
+
- Sorts of different types compose: they can be combined in one call
|
|
50
|
+
(`Property.sort(:name, tags: :name, addresses: :id)`) or chained
|
|
51
|
+
(`.sort(...).sort(...)`), sharing a single `GROUP BY`.
|
|
52
|
+
- Aggregates on a sorted relation (`count`, `sum`, `average`, `minimum`,
|
|
53
|
+
`maximum`) are computed over the records themselves rather than the
|
|
54
|
+
sort's grouped, joined rows — `count` returns the record count instead
|
|
55
|
+
of a per-group `Hash`, and multi-member records aren't weighted once
|
|
56
|
+
per member. A caller-supplied `group` still gets standard grouped
|
|
57
|
+
results.
|
|
58
|
+
- A blank direction (`''`, as query parameters often produce) is
|
|
59
|
+
accepted as ascending on all sort forms.
|
|
60
|
+
- `ActionController::Parameters` are accepted for relation sorts, and
|
|
61
|
+
their nulls option (`nulls_first`/`nulls_last`) is honored.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- `has_and_belongs_to_many` sorts crashed with `NoMethodError` on
|
|
66
|
+
ActiveRecord <= 8.0 (`Function#as` mutates and returns the receiver
|
|
67
|
+
there).
|
|
68
|
+
- Combining a `has_and_belongs_to_many` sort with another relation sort
|
|
69
|
+
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,11 +1,19 @@
|
|
|
1
1
|
# ActiveRecord::Sort
|
|
2
2
|
|
|
3
|
-
`ActiveRecord::Sort` provides
|
|
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
|
|
4
12
|
|
|
5
13
|
Installation
|
|
6
14
|
------------
|
|
7
15
|
|
|
8
|
-
Add `
|
|
16
|
+
Add `activerecord-sort` to your Gemfile and run `bundle`:
|
|
9
17
|
|
|
10
18
|
```ruby
|
|
11
19
|
gem 'activerecord-sort', require: 'active_record/sort'
|
|
@@ -15,8 +23,10 @@ Or install the gem and require it:
|
|
|
15
23
|
|
|
16
24
|
```sh
|
|
17
25
|
gem install activerecord-sort
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require 'active_record/sort'
|
|
20
30
|
```
|
|
21
31
|
|
|
22
32
|
Examples
|
|
@@ -33,25 +43,70 @@ Property.sort(:id, :name).to_sql
|
|
|
33
43
|
Property.sort(id: :desc).to_sql
|
|
34
44
|
# => "...ORDER BY properties.id DESC"
|
|
35
45
|
|
|
36
|
-
Property.sort(id: {asc: :nulls_first})
|
|
46
|
+
Property.sort(id: {asc: :nulls_first}).to_sql
|
|
37
47
|
# => "...ORDER BY properties.id ASC NULLS FIRST"
|
|
38
48
|
|
|
39
|
-
Property.sort(id: {asc: :nulls_last})
|
|
49
|
+
Property.sort(id: {asc: :nulls_last}).to_sql
|
|
40
50
|
# => "...ORDER BY properties.id ASC NULLS LAST"
|
|
41
51
|
```
|
|
42
52
|
|
|
43
|
-
It can also sort on relations
|
|
53
|
+
It can also sort on relations. A relation sort groups by the sorted table's
|
|
54
|
+
primary key — so each record appears once and records with no associated
|
|
55
|
+
rows are still included — and orders by an aggregate of the requested
|
|
56
|
+
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:
|
|
44
61
|
|
|
45
62
|
```ruby
|
|
46
63
|
Property.sort(addresses: :id).to_sql
|
|
47
|
-
# => "
|
|
48
|
-
# => "
|
|
64
|
+
# => "SELECT properties.* FROM properties
|
|
65
|
+
# => " LEFT OUTER JOIN addresses ON addresses.property_id = properties.id
|
|
66
|
+
# => " GROUP BY properties.id
|
|
67
|
+
# => " ORDER BY MIN(addresses.id) ASC"
|
|
49
68
|
|
|
50
69
|
Property.sort(addresses: {id: :desc}).to_sql
|
|
51
|
-
# => "...
|
|
52
|
-
|
|
70
|
+
# => "...ORDER BY MAX(addresses.id) DESC"
|
|
71
|
+
|
|
72
|
+
Property.sort(addresses: {id: {asc: :nulls_first}}).to_sql
|
|
73
|
+
# => "...ORDER BY MIN(addresses.id) ASC NULLS FIRST"
|
|
74
|
+
|
|
75
|
+
Property.sort(tags: :name).to_sql # has_and_belongs_to_many
|
|
76
|
+
# => "SELECT properties.* FROM properties
|
|
77
|
+
# => " LEFT OUTER JOIN properties_tags ON properties_tags.property_id = properties.id
|
|
78
|
+
# => " LEFT OUTER JOIN tags ON tags.id = properties_tags.tag_id
|
|
79
|
+
# => " GROUP BY properties.id
|
|
80
|
+
# => " ORDER BY MIN(tags.name) ASC"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A relation sort is order-only — it never adds or removes records — so
|
|
84
|
+
aggregates on a sorted relation (`count`, `sum`, `average`, `minimum`,
|
|
85
|
+
`maximum`) are computed over the records themselves, not the sort's
|
|
86
|
+
grouped and joined rows.
|
|
87
|
+
|
|
88
|
+
Order randomly:
|
|
53
89
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# => "
|
|
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
|
|
57
112
|
```
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
require 'active_record'
|
|
2
|
+
require 'active_record/relation'
|
|
3
|
+
|
|
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
|
|
19
|
+
|
|
20
|
+
# ordering:
|
|
21
|
+
# :id
|
|
22
|
+
# :name, :id
|
|
23
|
+
# :id => :desc
|
|
24
|
+
# :id => {:desc => :nulls_last}
|
|
25
|
+
# :listings => :id
|
|
26
|
+
# :listings => {:id => {:asc => :nulls_first}}
|
|
27
|
+
# :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
|
+
|
|
36
|
+
resource = all
|
|
37
|
+
ordering.compact!
|
|
38
|
+
ordering.flatten!
|
|
39
|
+
return resource if ordering.size == 0
|
|
40
|
+
|
|
41
|
+
ordering.each do |order|
|
|
42
|
+
order = Array(order)
|
|
43
|
+
order.each do |column_or_relation, options|
|
|
44
|
+
if column_or_relation.to_sym == :random
|
|
45
|
+
resource = resource.order(Arel::Nodes::RandomOrdering.new)
|
|
46
|
+
elsif self.column_names.include?(column_or_relation.to_s)
|
|
47
|
+
resource = resource.sort_for_column(self.arel_table[column_or_relation.to_s], options)
|
|
48
|
+
elsif reflect_on_association(column_or_relation.to_sym)
|
|
49
|
+
resource = resource.sort_for_relation(column_or_relation.to_sym, options)
|
|
50
|
+
else
|
|
51
|
+
raise InvalidSort.new("Unknown column #{column_or_relation}")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
resource
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Normalizes per-column sort options into [direction, nulls]. A blank
|
|
60
|
+
# direction — a bare column, or "" as query params often produce —
|
|
61
|
+
# means :asc.
|
|
62
|
+
# TODO: probably don't need to cast to sym
|
|
63
|
+
def sort_direction_and_nulls(options)
|
|
64
|
+
if options.is_a?(Hash) || options.class.name == "ActionController::Parameters"
|
|
65
|
+
[options.keys.first.to_sym, options.values.first.to_sym]
|
|
66
|
+
elsif options.blank?
|
|
67
|
+
[:asc, nil]
|
|
68
|
+
else
|
|
69
|
+
[options.to_s.downcase.to_sym, nil]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def sort_for_column(column, options)
|
|
74
|
+
direction, nulls = sort_direction_and_nulls(options)
|
|
75
|
+
|
|
76
|
+
if direction == :desc
|
|
77
|
+
self.order(Arel::Nodes::Descending.new(column, nulls))
|
|
78
|
+
elsif direction == :asc
|
|
79
|
+
self.order(Arel::Nodes::Ascending.new(column, nulls))
|
|
80
|
+
else
|
|
81
|
+
raise InvalidSort.new("Unknown ordering #{direction}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The sort key lives only in the ORDER BY — nothing is added to the
|
|
86
|
+
# select list. That keeps the ORDER BY self-contained (so pluck/ids,
|
|
87
|
+
# which replace the select list, keep the sort), leaves the caller's
|
|
88
|
+
# select untouched, and means loaded records carry exactly their own
|
|
89
|
+
# attributes (a selected join column would overwrite a same-named
|
|
90
|
+
# attribute on the base table).
|
|
91
|
+
def sort_for_relation(relation, options)
|
|
92
|
+
resource = self
|
|
93
|
+
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
|
+
|
|
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]
|
|
125
|
+
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}")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
resource = resource.left_outer_joins(relation.name)
|
|
138
|
+
resource = resource.order(order)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
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)
|
|
151
|
+
# Tag the relation for the count override below. Chained relations
|
|
152
|
+
# are built with clone, which copies instance variables, so the tag
|
|
153
|
+
# survives further chaining.
|
|
154
|
+
resource.instance_variable_set(:@sorted_by_relation, true)
|
|
155
|
+
resource
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
module Sort
|
|
161
|
+
module Calculations
|
|
162
|
+
# sort_for_relation groups by the primary key and LEFT JOINs the
|
|
163
|
+
# relation, so aggregates would see grouped, fanned-out rows: count
|
|
164
|
+
# returns a per-group Hash, and sum/average weigh a record once per
|
|
165
|
+
# collection member. But a relation sort is order-only — records are
|
|
166
|
+
# never added or removed — so every aggregate has a well-defined
|
|
167
|
+
# answer: compute it over the base table restricted to the sorted
|
|
168
|
+
# relation's (distinct) primary keys. User joins and conditions still
|
|
169
|
+
# apply inside the subquery, and a user-added group falls through to
|
|
170
|
+
# the standard grouped behavior.
|
|
171
|
+
def calculate(operation, column_name)
|
|
172
|
+
if @sorted_by_relation && group_values == [klass.arel_table[klass.primary_key]]
|
|
173
|
+
klass.where(klass.primary_key => unscope(:group, :order, :select).select(klass.primary_key))
|
|
174
|
+
.calculate(operation, column_name)
|
|
175
|
+
else
|
|
176
|
+
super
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
ActiveRecord::Relation.prepend(ActiveRecord::Sort::Calculations)
|
data/lib/active_record/sort.rb
CHANGED
|
@@ -1,126 +1,7 @@
|
|
|
1
1
|
require 'active_record'
|
|
2
|
-
require 'active_record/relation'
|
|
3
2
|
require 'arel/extensions'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
module Sort
|
|
7
|
-
|
|
8
|
-
# ordering:
|
|
9
|
-
# :id
|
|
10
|
-
# :name, :id
|
|
11
|
-
# :id => :desc
|
|
12
|
-
# :id => {:desc => :nulls_last}
|
|
13
|
-
# :listings => :id
|
|
14
|
-
# :listings => {:id => {:asc => :nulls_first}}
|
|
15
|
-
# :random
|
|
16
|
-
def sort(*ordering)
|
|
17
|
-
resource = all
|
|
18
|
-
ordering.compact!
|
|
19
|
-
ordering.flatten!
|
|
20
|
-
return resource if ordering.size == 0
|
|
21
|
-
|
|
22
|
-
ordering.each do |order|
|
|
23
|
-
order = Array(order)
|
|
24
|
-
|
|
25
|
-
order.each do |column_or_relation, options|
|
|
26
|
-
if column_or_relation.to_sym == :random
|
|
27
|
-
resource = resource.random_sort
|
|
28
|
-
elsif self.column_names.include?(column_or_relation.to_s)
|
|
29
|
-
resource = resource.sort_for_column(self.arel_table[column_or_relation.to_s], options)
|
|
30
|
-
elsif reflect_on_association(column_or_relation.to_sym)
|
|
31
|
-
resource = resource.select(resource.klass.arel_table[Arel::Nodes::SqlLiteral.new('*')])
|
|
32
|
-
resource = resource.sort_for_relation(column_or_relation.to_sym, options)
|
|
33
|
-
else
|
|
34
|
-
raise ActiveRecord::StatementInvalid.new("Unkown column #{column_or_relation}")
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
resource
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def random_sort
|
|
43
|
-
self.order(Arel::Nodes::RandomOrdering.new)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# TODO: probably don't need to cast to sym
|
|
47
|
-
def sort_for_column(column, options)
|
|
48
|
-
direction = (options.is_a?(Hash) || options.class.name == "ActionController::Parameters" ? options.keys.first.to_sym : options.to_s.downcase.to_sym)
|
|
49
|
-
|
|
50
|
-
nulls = (options.is_a?(Hash) ? options.values.first.to_sym : nil)
|
|
51
|
-
if direction == :desc
|
|
52
|
-
self.order(Arel::Nodes::Descending.new(column, nulls))
|
|
53
|
-
elsif direction == :asc || direction == :''
|
|
54
|
-
self.order(Arel::Nodes::Ascending.new(column, nulls))
|
|
55
|
-
else
|
|
56
|
-
raise ActiveRecord::StatementInvalid.new("Unkown ordering #{direction}")
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def sort_for_relation(relation, options)
|
|
61
|
-
resource = self
|
|
62
|
-
relation = reflect_on_association(relation)
|
|
63
|
-
|
|
64
|
-
if relation.macro == :has_many
|
|
65
|
-
options = [options] if !options.is_a?(Array)
|
|
66
|
-
|
|
67
|
-
options.each do |order|
|
|
68
|
-
order = Array(order)
|
|
69
|
-
order.each do |column, options|
|
|
70
|
-
column = Arel::Attributes::Relation.new(relation.klass.arel_table[column], relation.name)
|
|
71
|
-
direction = (options.is_a?(Hash) ? options.keys.first.to_sym : options.to_s.downcase.to_sym)
|
|
72
|
-
|
|
73
|
-
nulls = (options.is_a?(Hash) ? options.values.first.to_sym : nil)
|
|
74
|
-
if direction == :desc
|
|
75
|
-
# aggregation = Arel::Nodes::Max.new([column], "max_#{relation.name}_#{column.name}")
|
|
76
|
-
# order = Arel::Nodes::Descending.new(Arel::Nodes::SqlLiteral.new("max_#{relation.name}_#{column.name}"), nulls)
|
|
77
|
-
|
|
78
|
-
if relation.options[:through]
|
|
79
|
-
resource = resource.joins(relation.options[:through] => relation.source_reflection_name)
|
|
80
|
-
else
|
|
81
|
-
resource = resource.joins(relation.name)
|
|
82
|
-
end
|
|
83
|
-
# resource = resource.select(aggregation)
|
|
84
|
-
# resource = resource.order(order)
|
|
85
|
-
resource = resource.order(Arel::Nodes::Descending.new(column, nulls))
|
|
86
|
-
else
|
|
87
|
-
# aggregation = Arel::Nodes::Min.new([column], "min_#{relation.name}_#{column.name}")
|
|
88
|
-
order = Arel::Nodes::Ascending.new(Arel::Nodes::SqlLiteral.new("min_#{relation.name}_#{column.name}"), nulls)
|
|
89
|
-
|
|
90
|
-
resource = resource.joins(relation.name)
|
|
91
|
-
# resource = resource.select(aggregation)
|
|
92
|
-
# resource = resource.order(order)
|
|
93
|
-
resource = resource.order(Arel::Nodes::Ascending.new(column, nulls))
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
elsif relation.macro == :belongs_to || relation.macro == :has_one
|
|
98
|
-
options = [options] if !options.is_a?(Array)
|
|
99
|
-
|
|
100
|
-
options.each do |order|
|
|
101
|
-
order = Array(order)
|
|
102
|
-
order.each do |column, options|
|
|
103
|
-
column = relation.klass.arel_table[column]
|
|
104
|
-
direction = (options.is_a?(Hash) ? options.keys.first.to_sym : options.to_s.downcase.to_sym)
|
|
105
|
-
|
|
106
|
-
nulls = (options.is_a?(Hash) ? options.values.first.to_sym : nil)
|
|
107
|
-
if direction == :asc
|
|
108
|
-
order = Arel::Nodes::Ascending.new(column, nulls)
|
|
109
|
-
else
|
|
110
|
-
order = Arel::Nodes::Descending.new(column, nulls)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
resource = resource.left_outer_joins(relation.name)
|
|
114
|
-
resource = resource.order(order)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
resource
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
end
|
|
123
|
-
end
|
|
4
|
+
require File.expand_path(File.join(__FILE__, '../../../ext/active_record/base'))
|
|
124
5
|
|
|
125
6
|
ActiveRecord::QueryMethods.prepend(ActiveRecord::Sort)
|
|
126
7
|
ActiveRecord::Querying.delegate :sort, to: :all
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-sort
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 8.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon Bracy
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -16,28 +15,34 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 8.0.0
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
20
22
|
type: :runtime
|
|
21
23
|
prerelease: false
|
|
22
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
25
|
requirements:
|
|
24
26
|
- - ">="
|
|
25
27
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
28
|
+
version: 8.0.0
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
27
32
|
- !ruby/object:Gem::Dependency
|
|
28
33
|
name: arel-extensions
|
|
29
34
|
requirement: !ruby/object:Gem::Requirement
|
|
30
35
|
requirements:
|
|
31
36
|
- - ">="
|
|
32
37
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
38
|
+
version: 9.0.0
|
|
34
39
|
type: :runtime
|
|
35
40
|
prerelease: false
|
|
36
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
42
|
requirements:
|
|
38
43
|
- - ">="
|
|
39
44
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
45
|
+
version: 9.0.0
|
|
41
46
|
- !ruby/object:Gem::Dependency
|
|
42
47
|
name: pg
|
|
43
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,20 +57,6 @@ dependencies:
|
|
|
52
57
|
- - ">="
|
|
53
58
|
- !ruby/object:Gem::Version
|
|
54
59
|
version: '0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: bundler
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - ">="
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
62
|
-
type: :development
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - ">="
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
69
60
|
- !ruby/object:Gem::Dependency
|
|
70
61
|
name: rake
|
|
71
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -112,18 +103,18 @@ dependencies:
|
|
|
112
103
|
name: simplecov
|
|
113
104
|
requirement: !ruby/object:Gem::Requirement
|
|
114
105
|
requirements:
|
|
115
|
-
- -
|
|
106
|
+
- - '='
|
|
116
107
|
- !ruby/object:Gem::Version
|
|
117
|
-
version:
|
|
108
|
+
version: 1.0.0.rc5
|
|
118
109
|
type: :development
|
|
119
110
|
prerelease: false
|
|
120
111
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
112
|
requirements:
|
|
122
|
-
- -
|
|
113
|
+
- - '='
|
|
123
114
|
- !ruby/object:Gem::Version
|
|
124
|
-
version:
|
|
115
|
+
version: 1.0.0.rc5
|
|
125
116
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
117
|
+
name: faker
|
|
127
118
|
requirement: !ruby/object:Gem::Requirement
|
|
128
119
|
requirements:
|
|
129
120
|
- - ">="
|
|
@@ -137,35 +128,35 @@ dependencies:
|
|
|
137
128
|
- !ruby/object:Gem::Version
|
|
138
129
|
version: '0'
|
|
139
130
|
- !ruby/object:Gem::Dependency
|
|
140
|
-
name:
|
|
131
|
+
name: sunstone
|
|
141
132
|
requirement: !ruby/object:Gem::Requirement
|
|
142
133
|
requirements:
|
|
143
134
|
- - ">="
|
|
144
135
|
- !ruby/object:Gem::Version
|
|
145
|
-
version:
|
|
136
|
+
version: 7.0.0
|
|
146
137
|
type: :development
|
|
147
138
|
prerelease: false
|
|
148
139
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
140
|
requirements:
|
|
150
141
|
- - ">="
|
|
151
142
|
- !ruby/object:Gem::Version
|
|
152
|
-
version:
|
|
143
|
+
version: 7.0.0
|
|
153
144
|
- !ruby/object:Gem::Dependency
|
|
154
|
-
name:
|
|
145
|
+
name: webmock
|
|
155
146
|
requirement: !ruby/object:Gem::Requirement
|
|
156
147
|
requirements:
|
|
157
148
|
- - ">="
|
|
158
149
|
- !ruby/object:Gem::Version
|
|
159
|
-
version:
|
|
150
|
+
version: '0'
|
|
160
151
|
type: :development
|
|
161
152
|
prerelease: false
|
|
162
153
|
version_requirements: !ruby/object:Gem::Requirement
|
|
163
154
|
requirements:
|
|
164
155
|
- - ">="
|
|
165
156
|
- !ruby/object:Gem::Version
|
|
166
|
-
version:
|
|
157
|
+
version: '0'
|
|
167
158
|
- !ruby/object:Gem::Dependency
|
|
168
|
-
name:
|
|
159
|
+
name: debug
|
|
169
160
|
requirement: !ruby/object:Gem::Requirement
|
|
170
161
|
requirements:
|
|
171
162
|
- - ">="
|
|
@@ -178,42 +169,43 @@ dependencies:
|
|
|
178
169
|
- - ">="
|
|
179
170
|
- !ruby/object:Gem::Version
|
|
180
171
|
version: '0'
|
|
181
|
-
description:
|
|
182
|
-
|
|
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.'
|
|
183
175
|
email:
|
|
184
176
|
- jonbracy@gmail.com
|
|
185
177
|
executables: []
|
|
186
178
|
extensions: []
|
|
187
|
-
extra_rdoc_files:
|
|
188
|
-
- README.md
|
|
179
|
+
extra_rdoc_files: []
|
|
189
180
|
files:
|
|
181
|
+
- CHANGELOG.md
|
|
190
182
|
- LICENSE
|
|
191
183
|
- README.md
|
|
184
|
+
- ext/active_record/base.rb
|
|
192
185
|
- lib/active_record/sort.rb
|
|
193
186
|
- lib/active_record/sort/version.rb
|
|
194
187
|
homepage: https://github.com/malomalo/activerecord-sort
|
|
195
188
|
licenses:
|
|
196
189
|
- MIT
|
|
197
|
-
metadata:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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: []
|
|
202
195
|
require_paths:
|
|
203
196
|
- lib
|
|
204
197
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
205
198
|
requirements:
|
|
206
199
|
- - ">="
|
|
207
200
|
- !ruby/object:Gem::Version
|
|
208
|
-
version: '
|
|
201
|
+
version: '3.3'
|
|
209
202
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
203
|
requirements:
|
|
211
204
|
- - ">="
|
|
212
205
|
- !ruby/object:Gem::Version
|
|
213
206
|
version: '0'
|
|
214
207
|
requirements: []
|
|
215
|
-
rubygems_version:
|
|
216
|
-
signing_key:
|
|
208
|
+
rubygems_version: 4.0.11
|
|
217
209
|
specification_version: 4
|
|
218
210
|
summary: A safe way to accept user parameters and order against your ActiveRecord
|
|
219
211
|
Models
|