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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2139f5b59c57d0163c85a86b23b79a3e6fd00a28
4
- data.tar.gz: e879b1b335c61133b122c014e49b01acb83c907d
2
+ SHA256:
3
+ metadata.gz: 6b0284712fbd8e451e7bb4f41a81dc410c8141bc6e3d670dae192e02565bfb9f
4
+ data.tar.gz: 6b76ba2002e94c9f320f124ac4ba12bf786e213f9f7fcbcae20ae47402054dce
5
5
  SHA512:
6
- metadata.gz: a8c33923361af77ea70eaf89eed9486122ff82e70ebcb8dae13d8ee273155e0075a0207e59a7dc276a9d335548830b6a768db1575cc657e2d64c3ad58cbade63
7
- data.tar.gz: a42483806ef779802cf97467ebd53fa95e2e97639ce709e51564320ae184e09274a6aaa001f5c7a4414897e4c6918816d5b680466fd6b784c2384bc288167272
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
@@ -1,5 +1,6 @@
1
- source 'https://rubygems.org'
1
+ # frozen_string_literal: true
2
2
 
3
- gemspec
3
+ source 'https://rubygems.org'
4
4
 
5
- eval_gemfile './shared.gemfile'
5
+ eval_gemfile 'spec/gemfiles/rails_6_1.gemfile'
6
+ eval_gemfile 'rubocop.gemfile'
data/MIT-LICENSE CHANGED
@@ -1,4 +1,6 @@
1
- Copyright 2014 Gleb Mazovetskiy
1
+ Copyright (c) 2014-2017 Gleb Mazovetskiy
2
+
3
+ MIT License
2
4
 
3
5
  Permission is hereby granted, free of charge, to any person obtaining
4
6
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # order_query [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Coverage Status][coverage-badge]][coverage]
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.3.3'
14
+ gem 'order_query', '~> 0.5.1'
15
15
  ```
16
16
 
17
17
  ## Usage
18
18
 
19
- Define a named list of attributes to order by with `order_query(name, *order)`:
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 attributes is specified as:
31
+ Each order option specified in `order_query` is an array in the following form:
31
32
 
32
- 1. Attribute name.
33
- 2. Optionally, values to order by, such as `%w(high medium low)` or `[true, false]`.
34
- 3. Sort direction, `:asc` or `:desc`. Default: `:asc`; `:desc` when values to order by are specified.
35
- 4. Options:
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/github/glebm/order_query/badges/coverage.svg
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
- env = {'BUNDLE_GEMFILE' => gemfile}
19
- cmd_with_env = " (#{env.map { |k, v| "export #{k}=#{Shellwords.escape v}" } * ' '}; #{cmd})"
20
- $stderr.puts "Testing\n#{cmd_with_env}"
21
- PTY.spawn(env, cmd) do |r, _w, pid|
22
- begin
23
- r.each_line { |l| puts l }
24
- rescue Errno::EIO
25
- # Errno:EIO error means that the process has finished giving output.
26
- ensure
27
- ::Process.wait pid
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
- failed_cmds = statuses.reject(&:first).map { |(_status, cmd_with_env)| cmd_with_env }
33
- if failed_cmds.empty?
34
- $stderr.puts '✓ Tests pass with all gemfiles'
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 (#{failed_cmds.size} / #{statuses.size})\n#{failed_cmds * "\n"}"
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 (default: self.class.all)
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) || fst.is_a?(ActiveRecord::Base)
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)
@@ -1,45 +1,94 @@
1
- # coding: utf-8
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, :options
8
- delegate :column_name, :quote, to: :@sql
9
+ attr_reader :name, :order_enum, :custom_sql
10
+ delegate :column_name, :quote, :scope, to: :@sql_builder
9
11
 
10
- # @option spec [String] :unique Mark the attribute as unique to avoid redundant columns
11
- def initialize(spec, scope)
12
- spec = spec.dup
13
- options = spec.extract_options!
14
- @name = spec[0]
15
- if spec[1].is_a?(Array)
16
- @order_enum = spec.delete_at(1)
17
- spec[1] ||= :desc
18
- end
19
- @direction = Direction.parse!(spec[1] || :asc)
20
- @options = options.reverse_merge(
21
- unique: name.to_s == scope.primary_key,
22
- complete: true
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
- @unique = @options[:unique]
25
- @sql = SQL::Column.new(self, scope)
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 passed (depending on the side)
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
- @name,
61
- (@order_enum.inspect if order_enum),
62
- ('unique' if @unique),
63
- (column_name if options[:sql]),
64
- @direction
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
- extend self
6
+ module_function
5
7
 
6
- DIRECTIONS = [:asc, :desc].freeze
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, String] direction
20
+ # @param [:asc, :desc] direction
19
21
  # @raise [ArgumentError]
20
22
  # @return [:asc, :desc]
21
23
  def parse!(direction)
22
- if all.include?(direction)
23
- direction
24
- end or
25
- raise ArgumentError.new("sort direction must be in #{all.map(&:inspect).join(', ')}, is #{direction.inspect}")
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