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 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