order_query 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +5 -0
- data/Gemfile +1 -1
- data/README.md +88 -97
- data/lib/order_query/condition.rb +14 -3
- data/lib/order_query/point.rb +11 -22
- data/lib/order_query/space.rb +29 -11
- data/lib/order_query/sql/condition.rb +1 -2
- data/lib/order_query/sql/order_by.rb +7 -9
- data/lib/order_query/sql/where.rb +7 -6
- data/lib/order_query/version.rb +1 -1
- data/lib/order_query.rb +56 -20
- data/spec/order_query_spec.rb +50 -18
- data/spec/spec_helper.rb +3 -7
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02a08c0101e3e15512098357d8e9159941b2606f
|
4
|
+
data.tar.gz: a11460b00d5f8e3dc16dc28f0fc264d4c17f1fef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e450de5cb30e692f068ae9551459879a3315c9feed3527809a50f82412954ce258a495f54bbefa18f04e1a823310fd1eba55d44caf1d623bb94ac7712e0f905
|
7
|
+
data.tar.gz: b2c92a65bdb70275836a7fede9c457056fdbb9ba7fe768a8fe3b5608790264dec600ad857f504135a7d465905fa3201d7c740f03b71546d68bbd8ea1e92090af
|
data/CHANGES.md
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,132 +1,143 @@
|
|
1
|
-
# order_query [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Coverage Status][
|
1
|
+
# order_query [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Coverage Status][coverage-badge]][coverage]
|
2
2
|
|
3
|
-
|
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
|
-
|
6
|
-
|
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.
|
15
|
+
gem 'order_query', '~> 0.3.0'
|
20
16
|
```
|
21
17
|
|
22
18
|
## Usage
|
23
19
|
|
24
|
-
Define
|
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 :
|
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
|
-
|
38
|
-
|
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 |
|
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
|
-
|
47
|
+
### Scopes for `ORDER BY`
|
49
48
|
|
50
49
|
```ruby
|
51
|
-
Post.
|
52
|
-
Post.
|
50
|
+
Post.published.order_home #=> #<ActiveRecord::Relation>
|
51
|
+
Post.published.order_home_reverse #=> #<ActiveRecord::Relation>
|
53
52
|
```
|
54
53
|
|
55
|
-
### Before
|
54
|
+
### Before / after, previous / next, and position
|
56
55
|
|
57
|
-
|
56
|
+
First, get an `OrderQuery::Point` for the record:
|
58
57
|
|
59
58
|
```ruby
|
60
|
-
|
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
|
-
|
62
|
+
It exposes these finder methods:
|
73
63
|
|
74
64
|
```ruby
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
80
|
+
Even with looping, `nil` will be returned if there is only one record.
|
96
81
|
|
97
|
-
|
82
|
+
You can also get an `OrderQuery::Point` from an instance and a scope:
|
98
83
|
|
99
84
|
```ruby
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
90
|
+
### Dynamic conditions
|
91
|
+
|
92
|
+
Query with dynamic order conditions using the `seek(*spec)` class method:
|
107
93
|
|
108
94
|
```ruby
|
109
|
-
|
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
|
-
|
98
|
+
This returns an `OrderQuery::Space` that exposes these methods:
|
114
99
|
|
115
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
138
|
+
## How it works
|
128
139
|
|
129
|
-
|
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
|
-
[
|
182
|
-
[
|
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
|
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
|
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
|
-
|
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
|
data/lib/order_query/point.rb
CHANGED
@@ -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 :
|
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.
|
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
|
data/lib/order_query/space.rb
CHANGED
@@ -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]
|
12
|
-
# @param [Array<Array<Symbol,String
|
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
|
-
|
16
|
-
@
|
17
|
-
|
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
|
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
|
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(
|
8
|
-
@
|
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
|
-
|
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.
|
28
|
+
col_sql = cond.column_name
|
31
29
|
if cond.order_enum
|
32
|
-
cond.order_enum.map { |v| "#{col_sql}=#{cond.
|
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
|
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 =
|
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(
|
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.
|
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.
|
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.
|
120
|
+
["#{cond.column_name} #{op}#{'=' unless strict} ?".freeze, [from]]
|
120
121
|
end
|
121
122
|
|
122
123
|
WHERE_IDENTITY = [''.freeze, [].freeze].freeze
|
data/lib/order_query/version.rb
CHANGED
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
|
11
|
-
|
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
|
-
|
22
|
+
scope.seek(*spec).at(self)
|
14
23
|
end
|
15
24
|
|
16
25
|
module ClassMethods
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
31
|
-
#
|
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
|
-
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
|
data/spec/order_query_spec.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
14
|
-
|
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.
|
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 '.
|
122
|
-
ids =
|
123
|
-
expect(Issue.
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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 '#
|
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.
|
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, :
|
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')).
|
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')).
|
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
|
-
|
5
|
-
|
6
|
-
|
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.
|
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-
|
11
|
+
date: 2014-09-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|