order_query 0.2.1 → 0.3.0

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
2
  SHA1:
3
- metadata.gz: 04dd1e3cb69ac7b1cd363629e1449f2eec4bd921
4
- data.tar.gz: 1d2533afcfea18fe8a1b06ac63dec068ef59cf8f
3
+ metadata.gz: 02a08c0101e3e15512098357d8e9159941b2606f
4
+ data.tar.gz: a11460b00d5f8e3dc16dc28f0fc264d4c17f1fef
5
5
  SHA512:
6
- metadata.gz: f708515aa23195a7cb3051fca8848d1382190429ed9f2055cf0d0bc07b7b66b83c8792d5f1c3fdbbe6c7d3df2f62ec0571cc95755baeec621470c6464c838a05
7
- data.tar.gz: 7650728e46013f47e129d9a4e939ef531c687bda7315f8326737d628f1b8bee509395a072c079e342d04e2e34640ea9cde5b4f504eed25625ee05082c61f0a3a
6
+ metadata.gz: 9e450de5cb30e692f068ae9551459879a3315c9feed3527809a50f82412954ce258a495f54bbefa18f04e1a823310fd1eba55d44caf1d623bb94ac7712e0f905
7
+ data.tar.gz: b2c92a65bdb70275836a7fede9c457056fdbb9ba7fe768a8fe3b5608790264dec600ad857f504135a7d465905fa3201d7c740f03b71546d68bbd8ea1e92090af
data/CHANGES.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.3.0
2
+
3
+ * `order_query` now accepts conditions as varargs. Array form is still supported.
4
+ * `order_by` renamed to `seek`
5
+
1
6
  ## 0.2.1
2
7
 
3
8
  * `complete` now defaults to true for list attributes as well.
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :test, :development do
6
- gem 'coveralls', require: false
6
+ gem 'codeclimate-test-reporter', group: :test, require: nil
7
7
  gem 'byebug', platform: :mri_21, require: false
8
8
  end
9
9
 
data/README.md CHANGED
@@ -1,132 +1,143 @@
1
- # order_query [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Coverage Status][coveralls-badge]][coveralls]
1
+ # order_query [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Coverage Status][coverage-badge]][coverage]
2
2
 
3
- order_query gives you next or previous records relative to the current one efficiently.
3
+ <a href="http://use-the-index-luke.com/no-offset">
4
+ <img src="http://use-the-index-luke.com/img/no-offset.q200.png" alt="100% offset-free" align="right" width="106" height="106">
5
+ </a>
4
6
 
5
- For example, you have a list of items, sorted by priority. You have 10,000 items!
6
- If you are showing the user a single item, how do you provide buttons for the user to see the previous item or the next item?
7
-
8
- You could pass the item's position to the item page and use `OFFSET` in your SQL query.
9
- The downside of this, apart from having to pass a number that may change, is that the database cannot jump to the offset; it has to read every record until it reaches, say, the 9001st record.
10
- This is slow. Here is where `order_query` comes in!
11
-
12
- `order_query` uses the same `ORDER BY` query, but also includes a `WHERE` clause that excludes records before (for next) or after (for prev) the current one.
7
+ This gem finds the next or previous record(s) relative to the current one efficiently. It is also useful for implementing infinite scroll.
8
+ It uses [keyset pagination](http://use-the-index-luke.com/no-offset) to achieve this.
13
9
 
14
10
  ## Installation
15
11
 
16
12
  Add to Gemfile:
17
13
 
18
14
  ```ruby
19
- gem 'order_query', '~> 0.2.1'
15
+ gem 'order_query', '~> 0.3.0'
20
16
  ```
21
17
 
22
18
  ## Usage
23
19
 
24
- Define a list of order conditions with `order_query`:
20
+ Define named order conditions with `order_query`:
25
21
 
26
22
  ```ruby
27
23
  class Post < ActiveRecord::Base
28
24
  include OrderQuery
29
- order_query :order_for_index, [
25
+ order_query :order_home,
30
26
  [:pinned, [true, false]],
31
27
  [:published_at, :desc],
32
28
  [:id, :desc]
33
- ]
34
29
  end
35
30
  ```
36
31
 
37
- An order condition is specified as an attribute name, optionally an ordered list of values, and a sort direction.
38
- Additional options are:
32
+ Order query accepts a list of order conditions as varargs or one array, each one specified as:
33
+
34
+ ```ruby
35
+ [<attribute name>, (attribute values in order), (:asc or :desc), (options hash)]
36
+ ```
37
+
38
+ Available options:
39
39
 
40
40
  | option | description |
41
41
  |------------|----------------------------------------------------------------------------|
42
42
  | unique | Unique attribute. Default: `true` for primary key, `false` otherwise. |
43
- | complete | Enum attribute contains all the possible values. Default: `true`. |
43
+ | complete | Specified attribute values are the only possible values. Default: `true`. |
44
44
  | sql | Customize attribute value SQL |
45
45
 
46
- ### Order scopes
47
46
 
48
- Order scopes are defined by `order_query`:
47
+ ### Scopes for `ORDER BY`
49
48
 
50
49
  ```ruby
51
- Post.order_for_index #=> ActiveRecord::Relation<...>
52
- Post.reverse_order_for_index #=> ActiveRecord::Relation<...>
50
+ Post.published.order_home #=> #<ActiveRecord::Relation>
51
+ Post.published.order_home_reverse #=> #<ActiveRecord::Relation>
53
52
  ```
54
53
 
55
- ### Before, after, previous, and next
54
+ ### Before / after, previous / next, and position
56
55
 
57
- An method is added by `order_query` to query around a record:
56
+ First, get an `OrderQuery::Point` for the record:
58
57
 
59
58
  ```ruby
60
- # get the order object, scope default: Post.all
61
- p = Post.find(31).order_for_index(scope) #=> OrderQuery::RelativeOrder<...>
62
- p.before #=> ActiveRecord::Relation<...>
63
- p.previous #=> Post<...>
64
- # pass true to #next and #previous in order to loop onto the the first / last record
65
- # will not loop onto itself
66
- p.previous(true) #=> Post<...>
67
- p.position #=> 5
68
- p.next #=> Post<...>
69
- p.after #=> ActiveRecord::Relation<...>
59
+ p = Post.published.order_home_at(Post.find(31)) #=> #<OrderQuery::Point>
70
60
  ```
71
61
 
72
- #### Order conditions, advanced example
62
+ It exposes these finder methods:
73
63
 
74
64
  ```ruby
75
- class Issue < ActiveRecord::Base
76
- include OrderQuery
77
- order_query :order_display, [
78
- # Pass an array for attribute order, and an optional sort direction for the array,
79
- # default is *:desc*, so that first in the array <=> first in the result
80
- [:priority, %w(high medium low), :desc],
81
- # Sort attribute can be a method name, provided you pass :sql for the attribute
82
- [:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
83
- # Default sort order for non-array attributes is :asc, just like SQL
84
- [:updated_at, :desc],
85
- # pass unique: true for unique attributes to get more optimized queries
86
- # default: true for primary_key, false otherwise
87
- [:id, :desc, unique: true]
88
- ]
89
- def valid_votes_count
90
- votes - suspicious_votes
91
- end
92
- end
65
+ p.before #=> #<ActiveRecord::Relation>
66
+ p.after #=> #<ActiveRecord::Relation>
67
+ p.previous #=> #<Post>
68
+ p.next #=> #<Post>
69
+ p.position #=> 5
70
+ ```
71
+
72
+ Looping to the first / last record is enabled by default. Pass `false` to disable:
73
+
74
+ ```ruby
75
+ p = Post.order_home_at(Post.order_home.first)
76
+ p.previous #=> #<Post>
77
+ p.previous(false) #=> nil
93
78
  ```
94
79
 
95
- ### Dynamic order conditions
80
+ Even with looping, `nil` will be returned if there is only one record.
96
81
 
97
- To query with dynamic order conditions use `Model.order_by` and `Model#order_by`:
82
+ You can also get an `OrderQuery::Point` from an instance and a scope:
98
83
 
99
84
  ```ruby
100
- Issue.order_by([[:id, :desc]]) #=> ActiveRecord::Relation<...>
101
- Issue.visible.reverse_order_by([[:id, :desc]]) #=> ActiveRecord::Relation<...>
102
- Issue.find(31).order_by([[:id, :desc]]).next #=> Issue<...>
103
- Issue.find(31).order_by(Issue.visible, [[:id, :desc]]).next #=> Issue<...>
85
+ posts = Post.published
86
+ post = posts.find(42)
87
+ post.order_home(posts) #=> #<OrderQuery::Point>
104
88
  ```
105
89
 
106
- For example, consider ordering by a list of ids returned from an elasticsearch query:
90
+ ### Dynamic conditions
91
+
92
+ Query with dynamic order conditions using the `seek(*spec)` class method:
107
93
 
108
94
  ```ruby
109
- ids = Issue.keyword_search('ruby') #=> [7, 3, 5]
110
- Issue.where(id: ids).order_by([[:id, ids]]).first(2).to_a #=> [Issue<id=7>, Issue<id=3>]
95
+ space = Post.visible.seek([:id, :desc]) #=> #<OrderQuery::Space>
111
96
  ```
112
97
 
113
- ## How it works
98
+ This returns an `OrderQuery::Space` that exposes these methods:
114
99
 
115
- Internally this gem builds a query that depends on the current record's order values and looks like:
100
+ ```ruby
101
+ space.scope #=> #<ActiveRecord::Relation>
102
+ space.scope_reverse #=> #<ActiveRecord::Relation>
103
+ space.first #=> scope.first
104
+ space.last #=> scope_reverse.first
105
+ space.at(Post.first) #=> #<OrderQuery::Point>
106
+ ```
116
107
 
117
- ```sql
118
- SELECT ... WHERE
119
- x0 OR
120
- y0 AND (x1 OR
121
- y1 AND (x2 OR
122
- y2 AND ...))
123
- ORDER BY ...
124
- LIMIT 1
108
+ Alternatively, get an `OrderQuery::Point` using the `seek(scope, *spec)` instance method:
109
+
110
+ ```ruby
111
+ Post.find(42).seek(Post.visible, [:id, :desc]) #=> #<OrderQuery::Point>
112
+ # scope defaults to Post.all
113
+ Post.find(42).seek([:id, :desc]) #=> #<OrderQuery::Point>
114
+ ```
115
+
116
+ #### Advanced options example
117
+
118
+ ```ruby
119
+ class Post < ActiveRecord::Base
120
+ include OrderQuery
121
+ order_query :order_home,
122
+ # For an array of order values, default direction is :desc
123
+ # High-priority issues will be ordered first in this example
124
+ [:priority, %w(high medium low)],
125
+ # A method and custom SQL can be used instead of an attribute
126
+ [:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
127
+ # Default sort order for non-array conditions is :asc, just like SQL
128
+ [:updated_at, :desc],
129
+ # pass unique: true for unique attributes to get more optimized queries
130
+ # unique is true by default for primary_key
131
+ [:id, :desc]
132
+ def valid_votes_count
133
+ votes - suspicious_votes
134
+ end
135
+ end
125
136
  ```
126
137
 
127
- Where `x` correspond to `>` / `<` terms, and `y` to `=` terms (for resolving ties), per order criterion.
138
+ ## How it works
128
139
 
129
- A query may then look like this:
140
+ Internally this gem builds a query that depends on the current record's values and looks like this:
130
141
 
131
142
  ```sql
132
143
  -- Current post: pinned=true published_at='2014-03-21 15:01:35.064096' id=9
@@ -142,32 +153,12 @@ ORDER BY
142
153
  LIMIT 1
143
154
  ```
144
155
 
145
- A query for the advanced example would look like this:
146
-
147
- ```sql
148
- -- Current issue: priority='high' (votes - suspicious_votes)=4 updated_at='2014-03-19 10:23:18.671039' id=9
149
- SELECT "issues".* FROM "issues" WHERE
150
- ("issues"."priority" IN ('medium','low') OR
151
- "issues"."priority" = 'high' AND (
152
- (votes - suspicious_votes) < 4 OR
153
- (votes - suspicious_votes) = 4 AND (
154
- "issues"."updated_at" < '2014-03-19 10:23:18.671039' OR
155
- "issues"."updated_at" = '2014-03-19 10:23:18.671039' AND
156
- "issues"."id" < 9)))
157
- ORDER BY
158
- "issues"."priority"='high' DESC,
159
- "issues"."priority"='medium' DESC,
160
- "issues"."priority"='low' DESC,
161
- (votes - suspicious_votes) DESC,
162
- "issues"."updated_at" DESC,
163
- "issues"."id" DESC
164
- LIMIT 1
165
- ```
166
-
167
156
  The actual query is a bit different because `order_query` wraps the top-level `OR` with a (redundant) non-strict condition `x0' AND (x0 OR ...)`
168
157
  for [performance reasons](https://github.com/glebm/order_query/issues/3).
169
158
  This can be disabled with `OrderQuery.wrap_top_level_or = false`.
170
159
 
160
+ See the implementation in [sql/where.rb](/lib/order_query/sql/where.rb).
161
+
171
162
  See how this affects query planning in Markus Winand's slides on [Pagination done the Right Way](http://use-the-index-luke.com/blog/2013-07/pagination-done-the-postgresql-way).
172
163
 
173
164
  This project uses MIT license.
@@ -178,5 +169,5 @@ This project uses MIT license.
178
169
  [gemnasium]: https://gemnasium.com/glebm/order_query
179
170
  [codeclimate]: https://codeclimate.com/github/glebm/order_query
180
171
  [codeclimate-badge]: http://img.shields.io/codeclimate/github/glebm/order_query.svg
181
- [coveralls]: https://coveralls.io/r/glebm/order_query
182
- [coveralls-badge]: http://img.shields.io/coveralls/glebm/order_query.svg
172
+ [coverage]: https://codeclimate.com/github/glebm/order_query
173
+ [coverage-badge]: https://codeclimate.com/github/glebm/order_query/badges/coverage.svg
@@ -1,7 +1,10 @@
1
+ # coding: utf-8
1
2
  require 'order_query/sql/condition'
2
3
  module OrderQuery
4
+ # An order condition (sort column)
3
5
  class Condition
4
- attr_reader :name, :order, :order_enum, :options, :sql
6
+ attr_reader :name, :order, :order_enum, :options
7
+ delegate :column_name, :quote, to: :@sql
5
8
 
6
9
  # @option spec [String] :unique Mark the attribute as unique to avoid redundant conditions
7
10
  # @option spec [String] :complete Mark the condition's domain as complete to avoid redundant conditions (only for array conditions)
@@ -22,7 +25,7 @@ module OrderQuery
22
25
  )
23
26
  @unique = @options[:unique]
24
27
  @complete = @options[:complete]
25
- @sql = SQL::Condition.new(self, scope)
28
+ @sql = SQL::Condition.new(self, scope)
26
29
  end
27
30
 
28
31
  def unique?
@@ -56,7 +59,15 @@ module OrderQuery
56
59
  end
57
60
 
58
61
  def inspect
59
- "Condition(#{@name.inspect}#{" #{@order_enum.inspect}" if @order_enum} #{@order.to_s.upcase} #{'unique ' if @unique}#{@complete ? 'complete' : 'partial' if @order_enum})"
62
+ parts = [
63
+ @name,
64
+ (@order_enum.inspect if order_enum),
65
+ ('unique' if @unique),
66
+ ('complete' if order_enum && complete?),
67
+ (column_name if options[:sql]),
68
+ {desc: '▼', asc: '▲'}[@order]
69
+ ].compact
70
+ "(#{parts.join(' ')})"
60
71
  end
61
72
  end
62
73
  end
@@ -5,7 +5,7 @@ module OrderQuery
5
5
  # Search around a record in an order space
6
6
  class Point
7
7
  attr_reader :record, :space
8
- delegate :scope, :reverse_scope, to: :space
8
+ delegate :first, :last, :count, to: :space
9
9
 
10
10
  # @param [ActiveRecord::Base] record
11
11
  # @param [OrderQuery::Space] space
@@ -15,26 +15,6 @@ module OrderQuery
15
15
  @where_sql = SQL::Where.new(self)
16
16
  end
17
17
 
18
- # @return [ActiveRecord::Base]
19
- def first
20
- scope.first
21
- end
22
-
23
- # @return [ActiveRecord::Base]
24
- def last
25
- reverse_scope.first
26
- end
27
-
28
- # @return [Integer]
29
- def count
30
- @total ||= scope.count
31
- end
32
-
33
- # @return [Integer]
34
- def position
35
- count - after.count
36
- end
37
-
38
18
  # @params [true, false] loop if true, consider last and first as adjacent (unless they are equal)
39
19
  # @return [ActiveRecord::Base]
40
20
  def next(loop = true)
@@ -46,6 +26,11 @@ module OrderQuery
46
26
  unless_record_eq before.first || (last if loop)
47
27
  end
48
28
 
29
+ # @return [Integer]
30
+ def position
31
+ space.count - after.count
32
+ end
33
+
49
34
  # @return [ActiveRecord::Relation]
50
35
  def after
51
36
  side :after
@@ -63,7 +48,7 @@ module OrderQuery
63
48
  scope = if side == :after
64
49
  space.scope
65
50
  else
66
- space.reverse_scope
51
+ space.scope_reverse
67
52
  end
68
53
  if query.present?
69
54
  scope.where(query, *query_args)
@@ -76,6 +61,10 @@ module OrderQuery
76
61
  record.send(cond.name)
77
62
  end
78
63
 
64
+ def inspect
65
+ "#<OrderQuery::Point @record=#{@record.inspect} @space=#{@space.inspect}>"
66
+ end
67
+
79
68
  protected
80
69
 
81
70
  # @param [ActiveRecord::Base] rec
@@ -3,28 +3,46 @@ require 'order_query/sql/order_by'
3
3
  module OrderQuery
4
4
  # Order specification and a scope
5
5
  class Space
6
- # @return [Array<Condition>]
6
+ # @return [Array<OrderQuery::Condition>]
7
7
  attr_reader :conditions
8
- # @return [ActiveRecord::Relation]
9
- attr_reader :base_scope
10
8
 
11
- # @param [ActiveRecord::Relation] scope
12
- # @param [Array<Array<Symbol,String>>] order_spec
9
+ # @param [ActiveRecord::Relation] base_scope
10
+ # @param [Array<Array<Symbol,String>>, OrderQuery::Spec] order_spec
13
11
  def initialize(base_scope, order_spec)
14
12
  @base_scope = base_scope
15
- order_spec = [order_spec] unless order_spec.empty? || order_spec.first.is_a?(Array)
16
- @conditions = order_spec.map { |spec| Condition.new(spec, base_scope) }
17
- @order_by_sql = SQL::OrderBy.new(self)
13
+ @conditions = order_spec.map { |cond_spec| Condition.new(cond_spec, base_scope) }
14
+ @order_by_sql = SQL::OrderBy.new(@conditions)
15
+ end
16
+
17
+ # @return [Point]
18
+ def at(record)
19
+ Point.new(record, self)
18
20
  end
19
21
 
20
22
  # @return [ActiveRecord::Relation] scope ordered by conditions
21
23
  def scope
22
- @base_scope.order(@order_by_sql.build)
24
+ @scope ||= @base_scope.order(@order_by_sql.build)
23
25
  end
24
26
 
25
27
  # @return [ActiveRecord::Relation] scope ordered by conditions in reverse
26
- def reverse_scope
27
- @base_scope.order(@order_by_sql.build_reverse)
28
+ def scope_reverse
29
+ @scope_reverse ||= @base_scope.order(@order_by_sql.build_reverse)
30
+ end
31
+
32
+ # @return [ActiveRecord::Base]
33
+ def first
34
+ scope.first
35
+ end
36
+
37
+ # @return [ActiveRecord::Base]
38
+ def last
39
+ scope_reverse.first
40
+ end
41
+
42
+ delegate :count, :empty?, to: :@base_scope
43
+
44
+ def inspect
45
+ "#<OrderQuery::Space @conditions=#{@conditions.inspect} @base_scope=#{@base_scope.inspect}>"
28
46
  end
29
47
  end
30
48
  end
@@ -12,8 +12,7 @@ module OrderQuery
12
12
  @column_name ||= begin
13
13
  sql = condition.options[:sql]
14
14
  if sql
15
- sql = sql.call if sql.respond_to?(:call)
16
- sql
15
+ sql.respond_to?(:call) ? sql.call : sql
17
16
  else
18
17
  connection.quote_table_name(scope.table_name) + '.' + connection.quote_column_name(condition.name)
19
18
  end
@@ -1,35 +1,33 @@
1
1
  module OrderQuery
2
2
  module SQL
3
3
  class OrderBy
4
- attr_reader :space
5
-
6
4
  # @param [Array<Condition>]
7
- def initialize(space)
8
- @space = space
5
+ def initialize(conditions)
6
+ @conditions = conditions
9
7
  end
10
8
 
11
9
  # @return [String]
12
10
  def build
13
- join_order_by_clauses order_by_sql_clauses
11
+ @sql ||= join_order_by_clauses order_by_sql_clauses
14
12
  end
15
13
 
16
14
  # @return [String]
17
15
  def build_reverse
18
- join_order_by_clauses order_by_reverse_sql_clauses
16
+ @reverse_sql ||= join_order_by_clauses order_by_reverse_sql_clauses
19
17
  end
20
18
 
21
19
  protected
22
20
 
23
21
  # @return [Array<String>]
24
22
  def order_by_sql_clauses
25
- space.conditions.map { |cond| condition_clause cond }
23
+ @conditions.map { |cond| condition_clause cond }
26
24
  end
27
25
 
28
26
  def condition_clause(cond)
29
27
  dir_sql = sort_direction_sql cond.order
30
- col_sql = cond.sql.column_name
28
+ col_sql = cond.column_name
31
29
  if cond.order_enum
32
- cond.order_enum.map { |v| "#{col_sql}=#{cond.sql.quote v} #{dir_sql}" }.join(', ').freeze
30
+ cond.order_enum.map { |v| "#{col_sql}=#{cond.quote v} #{dir_sql}" }.join(', ').freeze
33
31
  else
34
32
  "#{col_sql} #{dir_sql}".freeze
35
33
  end
@@ -7,7 +7,8 @@ module OrderQuery
7
7
 
8
8
  # @param [OrderQuery::Point] point
9
9
  def initialize(point)
10
- @point = point
10
+ @point = point
11
+ @conditions = point.space.conditions
11
12
  end
12
13
 
13
14
  # Join condition pairs with OR, and nest within each other with AND
@@ -20,7 +21,7 @@ module OrderQuery
20
21
  # ... ))
21
22
  def build(side)
22
23
  # generate pairs of terms such as sales < 5, sales = 5
23
- parts = point.space.conditions.map { |cond|
24
+ parts = @conditions.map { |cond|
24
25
  [where_side(cond, side, true), where_tie(cond)].reject { |x| x == WHERE_IDENTITY }
25
26
  }
26
27
  # group pairwise with OR, and nest with AND
@@ -66,7 +67,7 @@ module OrderQuery
66
67
  top_pair_idx = pairs.index(&:present?)
67
68
  if top_pair_idx &&
68
69
  (top_pair = pairs[top_pair_idx]).length == 2 &&
69
- top_pair.first != (redundant_cond = where_side(point.space.conditions[top_pair_idx], side, false))
70
+ top_pair.first != (redundant_cond = where_side(@conditions[top_pair_idx], side, false))
70
71
  join_terms 'AND'.freeze, redundant_cond, wrap_term_with_parens(query)
71
72
  else
72
73
  query
@@ -104,19 +105,19 @@ module OrderQuery
104
105
  when 1
105
106
  where_eq cond, values[0]
106
107
  else
107
- ["#{cond.sql.column_name} IN (?)".freeze, [values]]
108
+ ["#{cond.column_name} IN (?)".freeze, [values]]
108
109
  end
109
110
  end
110
111
 
111
112
  def where_eq(cond, value = point.value(cond))
112
- [%Q(#{cond.sql.column_name} = ?).freeze, [value]]
113
+ [%Q(#{cond.column_name} = ?).freeze, [value]]
113
114
  end
114
115
 
115
116
  def where_ray(cond, from, mode, strict = true)
116
117
  ops = %w(< >)
117
118
  ops = ops.reverse if mode == :after
118
119
  op = {asc: ops[0], desc: ops[1]}[cond.order || :asc]
119
- ["#{cond.sql.column_name} #{op}#{'=' unless strict} ?".freeze, [from]]
120
+ ["#{cond.column_name} #{op}#{'=' unless strict} ?".freeze, [from]]
120
121
  end
121
122
 
122
123
  WHERE_IDENTITY = [''.freeze, [].freeze].freeze
@@ -1,3 +1,3 @@
1
1
  module OrderQuery
2
- VERSION = '0.2.1'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/order_query.rb CHANGED
@@ -6,20 +6,28 @@ require 'order_query/point'
6
6
  module OrderQuery
7
7
  extend ActiveSupport::Concern
8
8
 
9
- # @param [ActiveRecord::Relation] scope
10
- # @param [Array<Array<Symbol,String>>] order_spec
11
- def order_by(scope = nil, order_spec)
9
+ # @param [ActiveRecord::Relation] scope optional first argument (default: self.class.all)
10
+ # @param [Array<Array<Symbol,String>>, OrderQuery::Spec] order_spec
11
+ # @return [OrderQuery::Point]
12
+ # @example
13
+ # users = User.active
14
+ # user = users.find(42)
15
+ # next_user = user.seek(users, [:activated_at, :desc], [:id, :desc]).next
16
+ def seek(*spec)
17
+ fst = spec.first
18
+ if fst.nil? || fst.is_a?(ActiveRecord::Relation) || fst.is_a?(ActiveRecord::Base)
19
+ scope = spec.shift
20
+ end
12
21
  scope ||= self.class.all
13
- Point.new(self, Space.new(scope, order_spec))
22
+ scope.seek(*spec).at(self)
14
23
  end
15
24
 
16
25
  module ClassMethods
17
- def order_by(order_spec)
18
- Space.new(self, order_spec).scope
19
- end
20
-
21
- def reverse_order_by(order_spec)
22
- Space.new(self, order_spec).reverse_scope
26
+ # @return [OrderQuery::Space]
27
+ def seek(*spec)
28
+ # allow passing without a splat, as we can easily distinguish
29
+ spec = spec.first if spec.length == 1 && spec.first.first.is_a?(Array)
30
+ Space.new(all, spec)
23
31
  end
24
32
 
25
33
  #= DSL
@@ -27,18 +35,46 @@ module OrderQuery
27
35
  # @param [Symbol] name
28
36
  # @param [Array<Array<Symbol,String>>] order_spec
29
37
  # @example
30
- # class Issue
31
- # order_query :order_display, [[:created_at, :desc], [:id, :desc]]
38
+ # class Post < ActiveRecord::Base
39
+ # include OrderQuery
40
+ # order_query :order_home,
41
+ # [:pinned, [true, false]]
42
+ # [:published_at, :desc],
43
+ # [:id, :desc]
32
44
  # end
33
45
  #
34
- # Issue.order_display #=> <ActiveRecord::Relation#...>
35
- # Issue.active.find(31).display_order(Issue.active).next #=> <Issue#...>
36
- def order_query(name, order_spec)
37
- scope name, -> { order_by(order_spec) }
38
- scope :"reverse_#{name}", -> { reverse_order_by(order_spec) }
39
- define_method name do |scope = nil|
40
- order_by scope, order_spec
41
- end
46
+ #== Scopes
47
+ # .order_home
48
+ # #<ActiveRecord::Relation...>
49
+ # .order_home_reverse
50
+ # #<ActiveRecord::Relation...>
51
+ #
52
+ #== Class methods
53
+ # .order_home_at(post)
54
+ # #<OrderQuery::Point...>
55
+ # .order_home_space
56
+ # #<OrderQuery::Space...>
57
+ #
58
+ #== Instance methods
59
+ # .order_home(scope)
60
+ # #<OrderQuery::Point...>
61
+ def order_query(name, *spec)
62
+ space_method = :"#{name}_space"
63
+ define_singleton_method space_method, -> {
64
+ seek(*spec)
65
+ }
66
+ scope name, -> {
67
+ send(space_method).scope
68
+ }
69
+ scope :"#{name}_reverse", -> {
70
+ send(space_method).scope_reverse
71
+ }
72
+ define_singleton_method "#{name}_at", -> (record) {
73
+ send(space_method).at(record)
74
+ }
75
+ define_method(name) { |scope = nil|
76
+ (scope || self.class).send(space_method).at(self)
77
+ }
42
78
  end
43
79
  end
44
80
 
@@ -8,11 +8,10 @@ end
8
8
  # Simple model
9
9
  class Post < ActiveRecord::Base
10
10
  include OrderQuery
11
- order_query :order_list, [
12
- [:pinned, [true, false], complete: true],
13
- [:published_at, :desc],
14
- [:id, :desc]
15
- ]
11
+ order_query :order_list,
12
+ [:pinned, [true, false], complete: true],
13
+ [:published_at, :desc],
14
+ [:id, :desc]
16
15
  end
17
16
 
18
17
  def create_post(attr = {})
@@ -94,7 +93,7 @@ describe 'OrderQuery' do
94
93
  cur ||= issues.first
95
94
  expect(prev.display_order.next).to eq(cur)
96
95
  expect(cur.display_order.previous).to eq(prev)
97
- expect(cur.display_order.scope.count).to eq(Issue.count)
96
+ expect(cur.display_order.space.count).to eq(Issue.count)
98
97
  expect(cur.display_order.before.count + 1 + cur.display_order.after.count).to eq(cur.display_order.count)
99
98
 
100
99
  expect(cur.display_order.before.to_a.reverse + [cur] + cur.display_order.after.to_a).to eq(Issue.display_order.to_a)
@@ -118,21 +117,54 @@ describe 'OrderQuery' do
118
117
  expect(Issue.id_order_asc.count).to eq(2)
119
118
  end
120
119
 
121
- it '.order_by works on a list of ids' do
122
- ids = (1..3).map { create_issue.id }
123
- expect(Issue.order_by([[:id, ids]]).size).to eq ids.length
120
+ it '.seek works on a list of ids' do
121
+ ids = 3.times.map { create_issue.id }
122
+ expect(Issue.seek([[:id, ids]]).scope.count).to eq ids.length
123
+ expect(Issue.seek([:id, ids]).scope.count).to eq ids.length
124
124
  end
125
125
 
126
- it '.order_by preserves previous' do
127
- create_issue(active: true)
128
- expect(Issue.where(active: false).order_by([[:id, :desc]])).to be_empty
129
- expect(Issue.where(active: true).order_by([[:id, :desc]]).size).to eq 1
126
+ context 'partitioned on a boolean flag' do
127
+ before do
128
+ create_issue(active: true)
129
+ create_issue(active: false)
130
+ create_issue(active: true)
131
+ end
132
+
133
+ let!(:order) { [[:id, :desc]] }
134
+ let!(:active) { Issue.where(active: true).seek(order) }
135
+ let!(:inactive) { Issue.where(active: false).seek(order) }
136
+
137
+ it '.seek preserves scope' do
138
+ expect(inactive.scope.count).to eq 1
139
+ expect(inactive.count).to eq 1
140
+ expect(active.count).to eq 2
141
+ expect(active.scope.count).to eq 2
142
+ end
143
+
144
+ it 'gives a valid result if at argument is outside of the space' do
145
+ expect(inactive.at(active.first).next).to_not be_active
146
+ expect(inactive.at(active.last).next).to_not be_active
147
+ expect(active.at(inactive.first).next).to be_active
148
+ expect(active.at(inactive.last).next).to be_active
149
+ end
150
+
151
+ it 'next/previous(false)' do
152
+ expect(active.at(active.first).next(false)).to_not be_nil
153
+ expect(active.at(active.last).next(false)).to be_nil
154
+ expect(inactive.at(inactive.first).previous(false)).to be_nil
155
+ # there is only one, so previous(last) is also nil
156
+ expect(inactive.at(inactive.last).previous(false)).to be_nil
157
+ end
158
+
159
+ it 'previous(true) with only 1 record' do
160
+ expect(inactive.at(inactive.last).previous(true)).to be_nil
161
+ end
130
162
  end
131
163
 
132
- it '#order_by falls back to scope when order condition is missing self' do
164
+ it '#seek falls back to scope when order condition is missing self' do
133
165
  a = create_issue(priority: 'medium')
134
166
  b = create_issue(priority: 'high')
135
- expect(a.order_by(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
167
+ expect(a.seek(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
136
168
  end
137
169
 
138
170
  before do
@@ -149,7 +181,7 @@ describe 'OrderQuery' do
149
181
  t.column :suspicious_votes, :integer
150
182
  t.column :announced_at, :datetime
151
183
  t.column :updated_at, :datetime
152
- t.column :active, :boolen, null: false, default: true
184
+ t.column :active, :boolean, null: false, default: true
153
185
  end
154
186
  end
155
187
 
@@ -197,7 +229,7 @@ describe 'OrderQuery' do
197
229
  context 'wrap top-level OR on' do
198
230
  wrap_top_level_or true
199
231
  it 'wraps top-level OR' do
200
- after_scope = User.create!(updated_at: Date.parse('2014-09-06')).order_by([[:updated_at, :desc], [:id, :desc]]).after
232
+ after_scope = User.create!(updated_at: Date.parse('2014-09-06')).seek([[:updated_at, :desc], [:id, :desc]]).after
201
233
  expect(after_scope.to_sql).to include('<=')
202
234
  end
203
235
  end
@@ -205,7 +237,7 @@ describe 'OrderQuery' do
205
237
  context 'wrap top-level OR off' do
206
238
  wrap_top_level_or false
207
239
  it 'does not wrap top-level OR' do
208
- after_scope = User.create!(updated_at: Date.parse('2014-09-06')).order_by([[:updated_at, :desc], [:id, :desc]]).after
240
+ after_scope = User.create!(updated_at: Date.parse('2014-09-06')).seek([[:updated_at, :desc], [:id, :desc]]).after
209
241
  expect(after_scope.to_sql).to_not include('<=')
210
242
  end
211
243
  end
data/spec/spec_helper.rb CHANGED
@@ -1,13 +1,9 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  # Configure Rails Environment
3
3
  ENV['RAILS_ENV'] = ENV['RACK_ENV'] = 'test'
4
- unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
5
- begin
6
- require 'coveralls'
7
- Coveralls.wear!
8
- rescue LoadError
9
- false
10
- end
4
+ if ENV['TRAVIS'] && !(defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx')
5
+ require 'codeclimate-test-reporter'
6
+ CodeClimate::TestReporter.start
11
7
  end
12
8
  require 'order_query'
13
9
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: order_query
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Mazovetskiy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-06 00:00:00.000000000 Z
11
+ date: 2014-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord