typical_sort 0.1.0 → 0.1.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 +4 -4
- data/README.md +9 -1
- data/lib/typical_sort/aggregate_sorter.rb +45 -18
- data/lib/typical_sort/ordering.rb +21 -0
- data/lib/typical_sort/sort_query.rb +50 -0
- data/lib/typical_sort/sorter.rb +18 -18
- data/lib/typical_sort/version.rb +1 -1
- data/lib/typical_sort.rb +15 -40
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00ba97e42f71527115a64ddefd6d292678f9be903dca92dc416a6735f5efc38a
|
|
4
|
+
data.tar.gz: a62285eca9c248219e50d0ba36b0393a851b67cbc07ca754f1aaf7cf465449e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 259eccd6a42e98a2b95911874404d264600233c0c4d0220ab042ef9cd549b7778a6cd896a785f04be8e9547f663c265e689af95889f2e6a95c3b1e1eae395612
|
|
7
|
+
data.tar.gz: a4e3ad12e56cd73c329ea2590fb0fdb770e56ac0832ceded3bf23bd19a6c2120852e09269f1a51935c2722264e4f3970f9a289a509860f5481f2acba46894cba
|
data/README.md
CHANGED
|
@@ -104,7 +104,15 @@ Supported aggregate values:
|
|
|
104
104
|
- `:max`
|
|
105
105
|
- `:sum`
|
|
106
106
|
- `:avg`
|
|
107
|
-
- `:count`
|
|
107
|
+
- `:count` — counts associated records, including records whose declared sort column is null; parents without associated records count as zero
|
|
108
|
+
|
|
109
|
+
### Aggregate query composition
|
|
110
|
+
|
|
111
|
+
Aggregate sorting preserves filtering, deduplicates parents, and retains base-table column selections and ordering, readonly state, and preloads. Existing ordering takes priority over requested sorts, as it does for base-column sorting.
|
|
112
|
+
|
|
113
|
+
Filtering joins remain in an ID subquery. Associations requested through `eager_load` or `includes` are loaded separately with `preload`; these contain the full associated collections, not only the rows matching the filtering join.
|
|
114
|
+
|
|
115
|
+
Input selections and ordering must use base-table columns (symbols, plain column names, or Arel column/order nodes). Aggregate sorting raises `ArgumentError` for SQL expressions, joined-table projections or ordering, `GROUP BY` / `HAVING`, `DISTINCT`, custom `FROM`, or pagination applied before sorting. Use an explicit sorting scope for unsupported query shapes, and paginate after `sort_records`.
|
|
108
116
|
|
|
109
117
|
## Scope sorting
|
|
110
118
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "typical_sort/ordering"
|
|
4
|
+
|
|
3
5
|
module TypicalSort
|
|
4
6
|
class AggregateSorter
|
|
5
7
|
VALID_AGGREGATES = %i[min max sum avg count directional].freeze
|
|
@@ -8,11 +10,44 @@ module TypicalSort
|
|
|
8
10
|
|
|
9
11
|
# Collapse collection joins once, before applying any requested ordering.
|
|
10
12
|
def self.prepare(records)
|
|
13
|
+
validate_relation!(records)
|
|
14
|
+
|
|
11
15
|
table = records.klass.arel_table
|
|
12
|
-
|
|
13
|
-
records.
|
|
16
|
+
primary_key = records.klass.primary_key
|
|
17
|
+
base_ids = records.except(:select, :order).select(table[primary_key])
|
|
18
|
+
prepared = records.except(:where, :joins, :left_outer_joins, :references, :eager_load, :includes)
|
|
19
|
+
preloads = records.eager_load_values | records.includes_values
|
|
20
|
+
prepared = prepared.preload(*preloads) unless preloads.empty?
|
|
21
|
+
prepared.where(primary_key => base_ids)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.validate_relation!(records)
|
|
25
|
+
unsupported = records.group_values.any? || !records.having_clause.empty? ||
|
|
26
|
+
records.distinct_value || records.limit_value || records.offset_value ||
|
|
27
|
+
!records.from_clause.empty?
|
|
28
|
+
columns = records.select_values + records.order_values
|
|
29
|
+
return if !unsupported && columns.all? { |column| base_column?(column, records) }
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, "Unsupported aggregate sort relation: use base-table column selections and ordering without grouping, DISTINCT, custom FROM, or pagination; use an explicit sorting scope for other query shapes"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.base_column?(column, records)
|
|
35
|
+
case column
|
|
36
|
+
when Arel::Nodes::Ascending, Arel::Nodes::Descending, Arel::Nodes::NullsFirst, Arel::Nodes::NullsLast
|
|
37
|
+
base_column?(column.expr, records)
|
|
38
|
+
when Arel::Attributes::Attribute
|
|
39
|
+
column.relation == records.klass.arel_table
|
|
40
|
+
when Symbol, String
|
|
41
|
+
column = column.to_s
|
|
42
|
+
names = ["*", *records.klass.column_names]
|
|
43
|
+
names.any? { |name| column == name || column == "#{records.klass.table_name}.#{name}" }
|
|
44
|
+
else
|
|
45
|
+
false
|
|
46
|
+
end
|
|
14
47
|
end
|
|
15
48
|
|
|
49
|
+
private_class_method :validate_relation!, :base_column?
|
|
50
|
+
|
|
16
51
|
def initialize(records:, relation_name:, reflection:, column_name:, direction:, definition:, configuration:, table_alias: "typical_sort_scope")
|
|
17
52
|
@records = records
|
|
18
53
|
@relation_name = relation_name.to_sym
|
|
@@ -52,8 +87,12 @@ module TypicalSort
|
|
|
52
87
|
Arel::Nodes::On.new(sort_table[:typical_sort_record_id].eq(records.klass.arel_table[base_pk]))
|
|
53
88
|
)
|
|
54
89
|
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
ordering = Ordering.build(
|
|
91
|
+
sort_table[:typical_sort_value],
|
|
92
|
+
direction: direction,
|
|
93
|
+
nulls: definition.nulls || configuration.nulls.fetch(direction)
|
|
94
|
+
)
|
|
95
|
+
records.joins(join).order(ordering)
|
|
57
96
|
end
|
|
58
97
|
|
|
59
98
|
private
|
|
@@ -71,22 +110,10 @@ module TypicalSort
|
|
|
71
110
|
end
|
|
72
111
|
|
|
73
112
|
def value_sql(connection)
|
|
74
|
-
|
|
75
|
-
|
|
113
|
+
column = (definition.aggregate == :count) ? reflection.klass.primary_key : column_name
|
|
76
114
|
related_table = connection.quote_table_name(reflection.klass.table_name)
|
|
77
|
-
related_column = connection.quote_column_name(
|
|
115
|
+
related_column = connection.quote_column_name(column)
|
|
78
116
|
"#{related_table}.#{related_column}"
|
|
79
117
|
end
|
|
80
|
-
|
|
81
|
-
def sql_direction
|
|
82
|
-
(direction == :desc) ? "DESC" : "ASC"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def nulls_position
|
|
86
|
-
nulls = definition.nulls || configuration.nulls.fetch(direction)
|
|
87
|
-
raise ArgumentError, "Unsupported nulls position: #{nulls.inspect}" unless %i[first last].include?(nulls)
|
|
88
|
-
|
|
89
|
-
nulls.to_s.upcase
|
|
90
|
-
end
|
|
91
118
|
end
|
|
92
119
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypicalSort
|
|
4
|
+
module Ordering
|
|
5
|
+
def self.build(expression, direction:, nulls:)
|
|
6
|
+
ordered = case direction
|
|
7
|
+
when :asc then expression.asc
|
|
8
|
+
when :desc then expression.desc
|
|
9
|
+
else
|
|
10
|
+
raise ArgumentError, "Unsupported direction: #{direction.inspect}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
case nulls
|
|
14
|
+
when :first then ordered.nulls_first
|
|
15
|
+
when :last then ordered.nulls_last
|
|
16
|
+
else
|
|
17
|
+
raise ArgumentError, "Unsupported nulls position: #{nulls.inspect}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "typical_sort/sorter"
|
|
4
|
+
|
|
5
|
+
module TypicalSort
|
|
6
|
+
class SortQuery
|
|
7
|
+
def initialize(records:, sorts:, definitions:, configuration:)
|
|
8
|
+
@records = records
|
|
9
|
+
@sorts = sorts
|
|
10
|
+
@definitions = definitions
|
|
11
|
+
@configuration = configuration
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
sorts = resolve_sorts
|
|
16
|
+
sorted = Sorter.prepare(records: @records, definitions: sorts.map(&:first))
|
|
17
|
+
sorts.each_with_index do |(definition, direction), index|
|
|
18
|
+
sorted = Sorter.new(
|
|
19
|
+
records: sorted,
|
|
20
|
+
definition: definition,
|
|
21
|
+
direction: direction,
|
|
22
|
+
configuration: @configuration,
|
|
23
|
+
sort_index: index
|
|
24
|
+
).call
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
append_tiebreaker(sorted)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def resolve_sorts
|
|
33
|
+
@sorts.filter_map do |attribute, direction|
|
|
34
|
+
definition = @definitions.fetch(attribute)
|
|
35
|
+
if definition
|
|
36
|
+
[definition, direction]
|
|
37
|
+
elsif @configuration.invalid_sort == :raise
|
|
38
|
+
raise InvalidSort, "Invalid sort: #{attribute}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def append_tiebreaker(records)
|
|
44
|
+
return records unless @configuration.tie_breaker == :primary_key
|
|
45
|
+
|
|
46
|
+
table = records.klass.arel_table
|
|
47
|
+
records.order(table[records.klass.primary_key].asc)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/typical_sort/sorter.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "typical_sort/aggregate_sorter"
|
|
4
4
|
require "typical_sort/path_resolver"
|
|
5
|
+
require "typical_sort/ordering"
|
|
5
6
|
|
|
6
7
|
module TypicalSort
|
|
7
8
|
class Sorter
|
|
@@ -55,9 +56,18 @@ module TypicalSort
|
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def sort_relation_association(resolver)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
tracker = ActiveRecord::Associations::AliasTracker.create(
|
|
60
|
+
records.klass.connection_pool, records.klass.table_name, records.arel.join_sources
|
|
61
|
+
)
|
|
62
|
+
association = ActiveRecord::Associations::JoinDependency::JoinAssociation.new(resolver.reflection, [])
|
|
63
|
+
joins = association.join_constraints(records.klass.arel_table, records.klass, Arel::Nodes::OuterJoin, tracker) do |reflection, chain|
|
|
64
|
+
reflection.check_validity!
|
|
65
|
+
reflection.check_eager_loadable!
|
|
66
|
+
name = "typical_sort_relation_#{sort_index}_#{chain.length}"
|
|
67
|
+
[tracker.aliased_table_for(reflection.klass.arel_table, name) { name }, false]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
records.joins(joins).order(order_node(association.table[resolver.column_name]))
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
def validate_resolver!(resolver)
|
|
@@ -79,21 +89,11 @@ module TypicalSort
|
|
|
79
89
|
end
|
|
80
90
|
|
|
81
91
|
def order_node(node)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
ordered.nulls_first
|
|
88
|
-
when :last
|
|
89
|
-
ordered.nulls_last
|
|
90
|
-
else
|
|
91
|
-
raise ArgumentError, "Unsupported nulls position: #{nulls.inspect}"
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def nulls_position
|
|
96
|
-
definition.nulls || configuration.nulls.fetch(direction)
|
|
92
|
+
Ordering.build(
|
|
93
|
+
node,
|
|
94
|
+
direction: direction,
|
|
95
|
+
nulls: definition.nulls || configuration.nulls.fetch(direction)
|
|
96
|
+
)
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
end
|
data/lib/typical_sort/version.rb
CHANGED
data/lib/typical_sort.rb
CHANGED
|
@@ -6,7 +6,7 @@ require "typical_sort/engine"
|
|
|
6
6
|
require "typical_sort/configuration"
|
|
7
7
|
require "typical_sort/params"
|
|
8
8
|
require "typical_sort/sort_set"
|
|
9
|
-
require "typical_sort/
|
|
9
|
+
require "typical_sort/sort_query"
|
|
10
10
|
|
|
11
11
|
module TypicalSort
|
|
12
12
|
class Error < StandardError; end
|
|
@@ -49,53 +49,28 @@ module TypicalSort
|
|
|
49
49
|
private
|
|
50
50
|
|
|
51
51
|
def sort_records(records)
|
|
52
|
-
|
|
52
|
+
configuration = request_configuration
|
|
53
|
+
sorts = Params.new(
|
|
53
54
|
params: params,
|
|
54
|
-
configuration:
|
|
55
|
+
configuration: configuration,
|
|
55
56
|
default_attribute: default_sorting_attribute,
|
|
56
|
-
default_direction: default_sorting_direction
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
sorted = TypicalSort::Sorter.prepare(records: records, definitions: sorts.map(&:first))
|
|
69
|
-
sorts.each_with_index do |(definition, direction), index|
|
|
70
|
-
sorted = TypicalSort::Sorter.new(
|
|
71
|
-
records: sorted,
|
|
72
|
-
definition: definition,
|
|
73
|
-
direction: direction,
|
|
74
|
-
configuration: request_configuration,
|
|
75
|
-
sort_index: index
|
|
76
|
-
).call
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
append_sort_tiebreaker(sorted)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def sort_definition_for(attribute)
|
|
83
|
-
typical_sort_set.fetch(attribute)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def append_sort_tiebreaker(records)
|
|
87
|
-
return records unless TypicalSort.configuration.tie_breaker == :primary_key
|
|
88
|
-
|
|
89
|
-
table = records.klass.arel_table
|
|
90
|
-
records.order(table[records.klass.primary_key].asc)
|
|
57
|
+
default_direction: default_sorting_direction(configuration)
|
|
58
|
+
).sorts
|
|
59
|
+
|
|
60
|
+
SortQuery.new(
|
|
61
|
+
records: records,
|
|
62
|
+
sorts: sorts,
|
|
63
|
+
definitions: typical_sort_set,
|
|
64
|
+
configuration: configuration
|
|
65
|
+
).call
|
|
91
66
|
end
|
|
92
67
|
|
|
93
68
|
def default_sorting_attribute
|
|
94
69
|
typical_sort_set.default_attribute || :created_at
|
|
95
70
|
end
|
|
96
71
|
|
|
97
|
-
def default_sorting_direction
|
|
98
|
-
typical_sort_set.default_direction ||
|
|
72
|
+
def default_sorting_direction(configuration)
|
|
73
|
+
typical_sort_set.default_direction || configuration.default_direction
|
|
99
74
|
end
|
|
100
75
|
|
|
101
76
|
def request_configuration
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: typical_sort
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wyatt Kirby
|
|
@@ -192,9 +192,11 @@ files:
|
|
|
192
192
|
- lib/typical_sort/aggregate_sorter.rb
|
|
193
193
|
- lib/typical_sort/configuration.rb
|
|
194
194
|
- lib/typical_sort/engine.rb
|
|
195
|
+
- lib/typical_sort/ordering.rb
|
|
195
196
|
- lib/typical_sort/params.rb
|
|
196
197
|
- lib/typical_sort/path_resolver.rb
|
|
197
198
|
- lib/typical_sort/sort_definition.rb
|
|
199
|
+
- lib/typical_sort/sort_query.rb
|
|
198
200
|
- lib/typical_sort/sort_set.rb
|
|
199
201
|
- lib/typical_sort/sorter.rb
|
|
200
202
|
- lib/typical_sort/version.rb
|