order_query 0.3.3 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|