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