order_query 0.3.3 → 0.5.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 +5 -5
- data/CHANGES.md +34 -0
- data/Gemfile +4 -3
- data/MIT-LICENSE +3 -1
- data/README.md +25 -13
- data/Rakefile +26 -16
- data/lib/order_query.rb +12 -3
- data/lib/order_query/column.rb +74 -25
- data/lib/order_query/direction.rb +9 -7
- data/lib/order_query/errors.rb +11 -0
- data/lib/order_query/nulls_direction.rb +53 -0
- data/lib/order_query/point.rb +26 -9
- data/lib/order_query/space.rb +18 -8
- data/lib/order_query/sql/column.rb +9 -5
- data/lib/order_query/sql/order_by.rb +85 -11
- data/lib/order_query/sql/where.rb +63 -28
- data/lib/order_query/version.rb +3 -1
- data/spec/gemfiles/rails_5_0.gemfile +21 -0
- data/spec/gemfiles/rails_5_1.gemfile +21 -0
- data/spec/gemfiles/rails_5_2.gemfile +21 -0
- data/spec/gemfiles/rails_6_0.gemfile +21 -0
- data/spec/gemfiles/rails_6_1.gemfile +21 -0
- data/spec/gemfiles/rubocop.gemfile +5 -0
- data/spec/order_query_spec.rb +260 -67
- data/spec/spec_helper.rb +32 -6
- data/spec/support/order_expectation.rb +48 -0
- metadata +48 -27
- data/spec/gemfiles/rails_4.gemfile +0 -9
- data/spec/gemfiles/rails_4.gemfile.lock +0 -73
- data/spec/gemfiles/rails_5.gemfile +0 -8
- data/spec/gemfiles/rails_5.gemfile.lock +0 -76
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6b0284712fbd8e451e7bb4f41a81dc410c8141bc6e3d670dae192e02565bfb9f
|
4
|
+
data.tar.gz: 6b76ba2002e94c9f320f124ac4ba12bf786e213f9f7fcbcae20ae47402054dce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ddae4f82c3df302d84d2a7402aa5cc0aa2e6b4706a6d104b296ab5a33d036a121839fd01f42291364cc83347cc63e4a25e6776cbf776cf7e9c3e307818919f79
|
7
|
+
data.tar.gz: c1887c00922ab788bf659111e8ac55f7843c7a6dadfdf2d7948ca5ca1dd08a272142658e735c276d1b54c02b2fefb1d8fda1ebe49498d54cd76bc35435bda12a
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,37 @@
|
|
1
|
+
## 0.5.1
|
2
|
+
|
3
|
+
* Rails 6.1 now supported.
|
4
|
+
## 0.5.0
|
5
|
+
|
6
|
+
* Rails 6 now supported.
|
7
|
+
* Fixes support for `nil`s with explicit order, when a `nil` is neither
|
8
|
+
the first nor the last element of the explicit order,
|
9
|
+
e.g. `status: ['assigned', nil, 'fixed']`.
|
10
|
+
[#93b08877](https://github.com/glebm/order_query/commit/93b08877790a0ff02eea0d835def6ff3c40a83da)
|
11
|
+
|
12
|
+
## 0.4.1
|
13
|
+
|
14
|
+
* If a column had a `nulls:` option and there were multiple records with `NULL`,
|
15
|
+
all of these records but one were previously skipped. This is now fixed.
|
16
|
+
[#21](https://github.com/glebm/order_query/issues/21)
|
17
|
+
|
18
|
+
## 0.4.0
|
19
|
+
|
20
|
+
* Adds nulls ordering options `nulls: :first` and `nulls: :last`.
|
21
|
+
* Now supports Rails 5.2.
|
22
|
+
* Dropped support for Rails < 5 and Ruby < 2.3.
|
23
|
+
|
24
|
+
## 0.3.4
|
25
|
+
|
26
|
+
* The `before` and `after` methods now accept a boolean argument that indicates
|
27
|
+
whether the relation should exclude the given point or not.
|
28
|
+
By default the given point is excluded, if you want to include it,
|
29
|
+
use `before(false)` / `after(false)`.
|
30
|
+
|
31
|
+
## 0.3.3
|
32
|
+
|
33
|
+
* Now compatible with Rails 5 beta 1.
|
34
|
+
|
1
35
|
## 0.3.2
|
2
36
|
|
3
37
|
* Optimization: do not wrap top-level disjunctive in `AND` when the column has an enumerated order. [Read more](https://github.com/glebm/order_query/issues/3#issuecomment-54764638).
|
data/Gemfile
CHANGED
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# order_query [![Build Status][travis-badge]][travis] [![
|
1
|
+
# order_query [![Build Status][travis-badge]][travis] [![Coverage Status][coverage-badge]][coverage]
|
2
2
|
|
3
3
|
<a href="http://use-the-index-luke.com/no-offset">
|
4
4
|
<img src="http://use-the-index-luke.com/img/no-offset.q200.png" alt="100% offset-free" align="right" width="106" height="106">
|
@@ -11,33 +11,35 @@ This gem finds the next or previous record(s) relative to the current one effici
|
|
11
11
|
Add to Gemfile:
|
12
12
|
|
13
13
|
```ruby
|
14
|
-
gem 'order_query', '~> 0.
|
14
|
+
gem 'order_query', '~> 0.5.1'
|
15
15
|
```
|
16
16
|
|
17
17
|
## Usage
|
18
18
|
|
19
|
-
|
19
|
+
Use `order_query(scope_name, *order_option)` to create scopes and class methods
|
20
|
+
in your model and specify how you want results ordered. A basic example:
|
20
21
|
|
21
22
|
```ruby
|
22
23
|
class Post < ActiveRecord::Base
|
23
24
|
include OrderQuery
|
24
25
|
order_query :order_home,
|
25
|
-
[:pinned, [true, false]],
|
26
|
-
[:published_at, :desc]
|
26
|
+
[:pinned, [true, false]], # First sort by :pinned over t/f in :desc order
|
27
|
+
[:published_at, :desc] # Next sort :published_at in :desc order
|
27
28
|
end
|
28
29
|
```
|
29
30
|
|
30
|
-
Each
|
31
|
+
Each order option specified in `order_query` is an array in the following form:
|
31
32
|
|
32
|
-
1.
|
33
|
-
2.
|
34
|
-
3. Sort direction, `:asc` or `:desc
|
35
|
-
4.
|
33
|
+
1. Symbol of the attribute name (required).
|
34
|
+
2. An array of values to order by, such as `%w(high medium low)` or `[true, false]` (optional).
|
35
|
+
3. Sort direction, `:asc` or `:desc` (optional). Default: `:asc`; `:desc` when values to order by are specified.
|
36
|
+
4. A hash (optional):
|
36
37
|
|
37
38
|
| option | description |
|
38
39
|
|------------|----------------------------------------------------------------------------|
|
39
40
|
| unique | Unique attribute. Default: `true` for primary key, `false` otherwise. |
|
40
41
|
| sql | Customize column SQL. |
|
42
|
+
| nulls | If set to `:first` or `:last`, orders `NULL`s accordingly. |
|
41
43
|
|
42
44
|
If no unique column is specified, `[primary_key, :asc]` is used. Unique column must be last.
|
43
45
|
|
@@ -66,6 +68,18 @@ p.next #=> #<Post>
|
|
66
68
|
p.position #=> 5
|
67
69
|
```
|
68
70
|
|
71
|
+
The `before` and `after` methods also accept a boolean argument that indicates
|
72
|
+
whether the relation should exclude the given point or not.
|
73
|
+
By default the given point is excluded, if you want to include it,
|
74
|
+
use `before(false)` / `after(false)`.
|
75
|
+
|
76
|
+
If you want to obtain only a chunk (i.e., a page), use `before` or `after`
|
77
|
+
with ActiveRecord's `limit` method:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
p.after.limit(20) #=> #<ActiveRecord::Relation>
|
81
|
+
```
|
82
|
+
|
69
83
|
Looping to the first / last record is enabled for `next` / `previous` by default. Pass `false` to disable:
|
70
84
|
|
71
85
|
```ruby
|
@@ -170,7 +184,5 @@ This project uses MIT license.
|
|
170
184
|
[travis]: http://travis-ci.org/glebm/order_query
|
171
185
|
[travis-badge]: http://img.shields.io/travis/glebm/order_query.svg
|
172
186
|
[gemnasium]: https://gemnasium.com/glebm/order_query
|
173
|
-
[codeclimate]: https://codeclimate.com/github/glebm/order_query
|
174
|
-
[codeclimate-badge]: http://img.shields.io/codeclimate/github/glebm/order_query.svg
|
175
187
|
[coverage]: https://codeclimate.com/github/glebm/order_query
|
176
|
-
[coverage-badge]: https://codeclimate.com/
|
188
|
+
[coverage-badge]: https://api.codeclimate.com/v1/badges/82e424e9ee2acb02292c/test_coverage
|
data/Rakefile
CHANGED
@@ -1,39 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
begin
|
2
4
|
require 'bundler/setup'
|
3
5
|
rescue LoadError
|
4
6
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
7
|
end
|
6
8
|
|
9
|
+
require 'bundler/gem_tasks'
|
7
10
|
require 'rspec/core/rake_task'
|
8
11
|
RSpec::Core::RakeTask.new(:spec)
|
9
12
|
|
10
13
|
task default: :spec
|
11
14
|
|
15
|
+
# rubocop:disable Metrics/BlockLength,Style/StderrPuts
|
16
|
+
|
12
17
|
desc 'Test all Gemfiles from spec/*.gemfile'
|
13
18
|
task :test_all_gemfiles do
|
14
19
|
require 'pty'
|
15
20
|
require 'shellwords'
|
16
|
-
cmd = 'bundle --quiet && bundle exec rake --trace'
|
21
|
+
cmd = 'bundle install --quiet && bundle exec rake --trace'
|
17
22
|
statuses = Dir.glob('./spec/gemfiles/*{[!.lock]}').map do |gemfile|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
Bundler.with_clean_env do
|
24
|
+
env = { 'BUNDLE_GEMFILE' => gemfile }
|
25
|
+
$stderr.puts "Testing #{File.basename(gemfile)}:
|
26
|
+
export #{env.map { |k, v| "#{k}=#{Shellwords.escape v}" } * ' '}; #{cmd}"
|
27
|
+
PTY.spawn(env, cmd) do |r, _w, pid|
|
28
|
+
begin
|
29
|
+
r.each_line { |l| puts l }
|
30
|
+
rescue Errno::EIO # rubocop:disable Lint/HandleExceptions
|
31
|
+
# Errno:EIO error means that the process has finished giving output.
|
32
|
+
ensure
|
33
|
+
::Process.wait pid
|
34
|
+
end
|
28
35
|
end
|
36
|
+
[$CHILD_STATUS&.exitstatus&.zero?, gemfile]
|
29
37
|
end
|
30
|
-
[$? && $?.exitstatus == 0, cmd_with_env]
|
31
38
|
end
|
32
|
-
|
33
|
-
if
|
34
|
-
$stderr.puts
|
39
|
+
failed_gemfiles = statuses.reject(&:first).map { |(_, gemfile)| gemfile }
|
40
|
+
if failed_gemfiles.empty?
|
41
|
+
$stderr.puts "✓ Tests pass with all #{statuses.size} gemfiles"
|
35
42
|
else
|
36
|
-
$stderr.puts "❌ FAILING (#{
|
43
|
+
$stderr.puts "❌ FAILING (#{failed_gemfiles.size} / #{statuses.size})
|
44
|
+
#{failed_gemfiles * "\n"}"
|
37
45
|
exit 1
|
38
46
|
end
|
39
47
|
end
|
48
|
+
|
49
|
+
# rubocop:enable Metrics/BlockLength,Style/StderrPuts
|
data/lib/order_query.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support'
|
2
4
|
require 'active_record'
|
3
5
|
require 'order_query/space'
|
4
6
|
require 'order_query/point'
|
5
7
|
|
8
|
+
# This gem finds the next or previous record(s) relative to the current one
|
9
|
+
# efficiently using keyset pagination, e.g. for navigation or infinite scroll.
|
6
10
|
module OrderQuery
|
7
11
|
extend ActiveSupport::Concern
|
8
12
|
|
9
|
-
# @param [ActiveRecord::Relation] scope optional first argument
|
13
|
+
# @param [ActiveRecord::Relation] scope optional first argument
|
14
|
+
# (default: self.class.all)
|
10
15
|
# @param [Array<Array<Symbol,String>>, OrderQuery::Spec] order_spec
|
11
16
|
# @return [OrderQuery::Point]
|
12
17
|
# @example
|
@@ -15,13 +20,15 @@ module OrderQuery
|
|
15
20
|
# next_user = user.seek(users, [:activated_at, :desc], [:id, :desc]).next
|
16
21
|
def seek(*spec)
|
17
22
|
fst = spec.first
|
18
|
-
if fst.nil? || fst.is_a?(ActiveRecord::Relation) ||
|
23
|
+
if fst.nil? || fst.is_a?(ActiveRecord::Relation) ||
|
24
|
+
fst.is_a?(ActiveRecord::Base)
|
19
25
|
scope = spec.shift
|
20
26
|
end
|
21
27
|
scope ||= self.class.all
|
22
28
|
scope.seek(*spec).at(self)
|
23
29
|
end
|
24
30
|
|
31
|
+
# Top-level functions.
|
25
32
|
module ClassMethods
|
26
33
|
# @return [OrderQuery::Space]
|
27
34
|
def seek(*spec)
|
@@ -31,7 +38,9 @@ module OrderQuery
|
|
31
38
|
end
|
32
39
|
|
33
40
|
#= DSL
|
41
|
+
|
34
42
|
protected
|
43
|
+
|
35
44
|
# @param [Symbol] name
|
36
45
|
# @param [Array<Array<Symbol,String>>] order_spec
|
37
46
|
# @example
|
@@ -60,7 +69,7 @@ module OrderQuery
|
|
60
69
|
# #<OrderQuery::Point...>
|
61
70
|
def order_query(name, *spec)
|
62
71
|
define_singleton_method(:"#{name}_space") { seek(*spec) }
|
63
|
-
class_eval <<-RUBY, __FILE__, __LINE__
|
72
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
64
73
|
scope :#{name}, -> { #{name}_space.scope }
|
65
74
|
scope :#{name}_reverse, -> { #{name}_space.scope_reverse }
|
66
75
|
def self.#{name}_at(record)
|
data/lib/order_query/column.rb
CHANGED
@@ -1,45 +1,94 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'order_query/direction'
|
4
|
+
require 'order_query/nulls_direction'
|
3
5
|
require 'order_query/sql/column'
|
4
6
|
module OrderQuery
|
5
7
|
# An order column (sort column)
|
6
8
|
class Column
|
7
|
-
attr_reader :name, :order_enum, :
|
8
|
-
delegate :column_name, :quote, to: :@
|
9
|
+
attr_reader :name, :order_enum, :custom_sql
|
10
|
+
delegate :column_name, :quote, :scope, to: :@sql_builder
|
9
11
|
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
12
|
+
# rubocop:disable Metrics/ParameterLists,Metrics/AbcSize
|
13
|
+
# rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
14
|
+
# rubocop:disable Metrics/MethodLength
|
15
|
+
|
16
|
+
# @param scope [ActiveRecord::Relation]
|
17
|
+
# @param attr_name [Symbol] the name of the column, or the method providing
|
18
|
+
# the value to sort by.
|
19
|
+
# @param vals_and_or_dir [Array] optionally, values in the desired order,
|
20
|
+
# and / or one of `:asc`, `:desc`. Default order is `:desc` if the values
|
21
|
+
# are given (so the result is ordered like the values), `:asc` otherwise.
|
22
|
+
# @param unique [Boolean] mark the attribute as unique to avoid redundant
|
23
|
+
# columns. Default: `true` for primary key.
|
24
|
+
# @param nulls [:first, :last, false] whether to consider NULLS to be
|
25
|
+
# ordered first or last. If false, assumes that a column is not nullable
|
26
|
+
# and raises [Errors::NonNullableColumnIsNullError] if a null is
|
27
|
+
# encountered.
|
28
|
+
# @param sql [String, nil] a custom sql fragment.
|
29
|
+
def initialize(scope, attr_name, *vals_and_or_dir,
|
30
|
+
unique: nil, nulls: false, sql: nil)
|
31
|
+
@name = attr_name
|
32
|
+
@order_enum = vals_and_or_dir.shift if vals_and_or_dir[0].is_a?(Array)
|
33
|
+
@direction = Direction.parse!(
|
34
|
+
vals_and_or_dir.shift || (@order_enum ? :desc : :asc)
|
23
35
|
)
|
24
|
-
|
25
|
-
|
36
|
+
unless vals_and_or_dir.empty?
|
37
|
+
fail ArgumentError,
|
38
|
+
"extra arguments: #{vals_and_or_dir.map(&:inspect) * ', '}"
|
39
|
+
end
|
40
|
+
@unique = unique.nil? ? (name.to_s == scope.primary_key) : unique
|
41
|
+
if @order_enum&.include?(nil)
|
42
|
+
fail ArgumentError, '`nulls` cannot be set if a value is null' if nulls
|
43
|
+
|
44
|
+
@nullable = true
|
45
|
+
@nulls = if @order_enum[0].nil?
|
46
|
+
@direction == :desc ? :first : :last
|
47
|
+
else
|
48
|
+
@direction == :desc ? :last : :first
|
49
|
+
end
|
50
|
+
else
|
51
|
+
@nullable = !!nulls # rubocop:disable Style/DoubleNegation
|
52
|
+
@nulls = NullsDirection.parse!(
|
53
|
+
nulls || NullsDirection.default(scope, @direction)
|
54
|
+
)
|
55
|
+
end
|
56
|
+
@custom_sql = sql
|
57
|
+
@sql_builder = SQL::Column.new(scope, self)
|
26
58
|
end
|
59
|
+
# rubocop:enable Metrics/ParameterLists,Metrics/AbcSize
|
60
|
+
# rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
61
|
+
# rubocop:enable Metrics/MethodLength
|
27
62
|
|
28
63
|
def direction(reverse = false)
|
29
64
|
reverse ? Direction.reverse(@direction) : @direction
|
30
65
|
end
|
31
66
|
|
67
|
+
# @return [:first, :last]
|
68
|
+
def nulls_direction(reverse = false)
|
69
|
+
reverse ? NullsDirection.reverse(@nulls) : @nulls
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [:first, :last]
|
73
|
+
def default_nulls_direction(reverse = false)
|
74
|
+
NullsDirection.default(scope, direction(reverse))
|
75
|
+
end
|
76
|
+
|
77
|
+
def nullable?
|
78
|
+
@nullable
|
79
|
+
end
|
80
|
+
|
32
81
|
def unique?
|
33
82
|
@unique
|
34
83
|
end
|
35
84
|
|
36
85
|
# @param [Object] value
|
37
86
|
# @param [:before, :after] side
|
38
|
-
# @return [Array] valid order values before / after
|
87
|
+
# @return [Array] valid order values before / after the given value.
|
39
88
|
# @example for [:difficulty, ['Easy', 'Normal', 'Hard']]:
|
40
89
|
# enum_side('Normal', :after) #=> ['Hard']
|
41
90
|
# enum_side('Normal', :after, false) #=> ['Normal', 'Hard']
|
42
|
-
def enum_side(value, side, strict = true)
|
91
|
+
def enum_side(value, side, strict = true) # rubocop:disable Metrics/AbcSize
|
43
92
|
ord = order_enum
|
44
93
|
pos = ord.index(value)
|
45
94
|
if pos
|
@@ -57,11 +106,11 @@ module OrderQuery
|
|
57
106
|
|
58
107
|
def inspect
|
59
108
|
parts = [
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
109
|
+
@name,
|
110
|
+
(@order_enum.inspect if order_enum),
|
111
|
+
('unique' if @unique),
|
112
|
+
(column_name if @custom_sql),
|
113
|
+
@direction
|
65
114
|
].compact
|
66
115
|
"(#{parts.join(' ')})"
|
67
116
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module OrderQuery
|
2
4
|
# Responsible for handling :asc and :desc
|
3
5
|
module Direction
|
4
|
-
|
6
|
+
module_function
|
5
7
|
|
6
|
-
DIRECTIONS = [
|
8
|
+
DIRECTIONS = %i[asc desc].freeze
|
7
9
|
|
8
10
|
def all
|
9
11
|
DIRECTIONS
|
@@ -15,14 +17,14 @@ module OrderQuery
|
|
15
17
|
all[(all.index(direction) + 1) % 2].to_sym
|
16
18
|
end
|
17
19
|
|
18
|
-
# @param [:asc, :desc
|
20
|
+
# @param [:asc, :desc] direction
|
19
21
|
# @raise [ArgumentError]
|
20
22
|
# @return [:asc, :desc]
|
21
23
|
def parse!(direction)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
all.include?(direction) && direction or
|
25
|
+
fail ArgumentError,
|
26
|
+
"sort direction must be in #{all.map(&:inspect).join(', ')}, "\
|
27
|
+
"is #{direction.inspect}"
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OrderQuery
|
4
|
+
# All the exceptions that can be raised by order query methods.
|
5
|
+
module Errors
|
6
|
+
# Raised when a column that OrderQuery assumes to never contain NULLs
|
7
|
+
# contains a null.
|
8
|
+
class NonNullableColumnIsNullError < RuntimeError
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|