order_query 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +7 -0
- data/README.md +89 -25
- data/lib/order_query/relative_order.rb +5 -1
- data/lib/order_query/version.rb +1 -1
- data/spec/order_query_spec.rb +127 -83
- metadata +15 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e1364db02e6ba3b3d20bf11d77103dc33a1d287
|
4
|
+
data.tar.gz: 70f8f66433ef24caebe56b83a165073ac7377f88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ea5b0e8beb0268407ce15504ce0d57200c41fe4f059782c220c7c2b84c40205dd12123eb61f67e9b01d1c287b836123424ae6925d585142bea19b0077d2cf78
|
7
|
+
data.tar.gz: 3a9dc43bab64321c33f27d2869f895cb36efab27fea662919e61a49d7454863fef8c83ee16a50350b6beb789b51f86936b80636ecac4dfdf1b948a44ce4b0057
|
data/CHANGES.md
ADDED
data/README.md
CHANGED
@@ -1,54 +1,94 @@
|
|
1
1
|
# order_query [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Coverage Status][coveralls-badge]][coveralls]
|
2
2
|
|
3
|
-
order_query
|
4
|
-
|
3
|
+
order_query gives you next or previous records relative to the current one efficiently.
|
4
|
+
|
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.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add to Gemfile:
|
5
17
|
|
6
18
|
```ruby
|
7
|
-
gem 'order_query', '~> 0.1.
|
19
|
+
gem 'order_query', '~> 0.1.1'
|
8
20
|
```
|
9
21
|
|
10
22
|
## Usage
|
11
23
|
|
24
|
+
Define the criteria with `order_query`:
|
25
|
+
|
12
26
|
```ruby
|
13
|
-
class
|
27
|
+
class Post < ActiveRecord::Base
|
14
28
|
include OrderQuery
|
15
|
-
order_query :
|
16
|
-
[:
|
17
|
-
[:
|
18
|
-
[:
|
19
|
-
# pass unique: true for unique attributes to get more optimized queries
|
20
|
-
# default: true for primary_key, false otherwise
|
21
|
-
[:id, :desc, unique: true]
|
29
|
+
order_query :order_list, [
|
30
|
+
[:pinned, [true, false]],
|
31
|
+
[:published_at, :desc],
|
32
|
+
[:id, :desc]
|
22
33
|
]
|
23
|
-
def valid_votes_count
|
24
|
-
votes - suspicious_votes
|
25
|
-
end
|
26
34
|
end
|
27
35
|
```
|
28
36
|
|
29
|
-
Order scopes
|
37
|
+
### Order scopes
|
38
|
+
|
39
|
+
Defining the criteria adds `ORDER BY` scopes:
|
30
40
|
|
31
41
|
```ruby
|
32
|
-
|
33
|
-
|
42
|
+
Post.order_list #=> ActiveRecord::Relation<...>
|
43
|
+
Post.reverse_order_list #=> ActiveRecord::Relation<...>
|
34
44
|
```
|
35
45
|
|
36
|
-
Relative order
|
46
|
+
### Relative order
|
47
|
+
|
48
|
+
`order_query` also adds an instance method for querying relative to the record:
|
37
49
|
|
38
50
|
```ruby
|
39
|
-
# get the order object, scope default:
|
40
|
-
p =
|
51
|
+
# get the order object, scope default: Post.all
|
52
|
+
p = Post.find(31).order_list(scope) #=> OrderQuery::RelativeOrder<...>
|
41
53
|
p.before #=> ActiveRecord::Relation<...>
|
42
|
-
p.previous #=>
|
54
|
+
p.previous #=> Post<...>
|
43
55
|
# pass true to #next and #previous in order to loop onto the the first / last record
|
44
56
|
# will not loop onto itself
|
45
|
-
p.previous(true) #=>
|
57
|
+
p.previous(true) #=> Post<...>
|
46
58
|
p.position #=> 5
|
47
|
-
p.next #=>
|
59
|
+
p.next #=> Post<...>
|
48
60
|
p.after #=> ActiveRecord::Relation<...>
|
49
61
|
```
|
50
62
|
|
51
|
-
|
63
|
+
### Advanced options
|
64
|
+
|
65
|
+
There is a number of advanced options to help you:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class Issue < ActiveRecord::Base
|
69
|
+
include OrderQuery
|
70
|
+
order_query :order_display, [
|
71
|
+
# Pass an array for attribute order, and an optional sort direction for the array,
|
72
|
+
# default is *:desc*, so that first in the array <=> first in the result
|
73
|
+
[:priority, %w(high medium low), :desc],
|
74
|
+
# Sort attribute can be a method name, provided you pass :sql for the attribute
|
75
|
+
[:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
|
76
|
+
# Default sort order for non-array attributes is :asc, just like SQL
|
77
|
+
[:updated_at, :desc],
|
78
|
+
# pass unique: true for unique attributes to get more optimized queries
|
79
|
+
# default: true for primary_key, false otherwise
|
80
|
+
[:id, :desc, unique: true]
|
81
|
+
]
|
82
|
+
def valid_votes_count
|
83
|
+
votes - suspicious_votes
|
84
|
+
end
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
### Dynamic criteria
|
89
|
+
|
90
|
+
Including `OrderQuery` adds `.order_by_query` and `#relative_order_by_query`.
|
91
|
+
These methods can be called directly directly with the order criteria:
|
52
92
|
|
53
93
|
```ruby
|
54
94
|
Issue.order_by_query([[:id, :desc]]) #=> ActiveRecord::Relation<...>
|
@@ -57,6 +97,14 @@ Issue.find(31).relative_order_by_query([[:id, :desc]]).next #=> Issue<...>
|
|
57
97
|
Issue.find(31).relative_order_by_query(Issue.visible, [[:id, :desc]]).next #=> Issue<...>
|
58
98
|
```
|
59
99
|
|
100
|
+
This is especially helpful if the order criteria is dynamic, so `order_query` cannot be used to define them beforehand.
|
101
|
+
For example, consider ordering by a list of ids returned from an elasticsearh query:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
ids = Issue.keyword_search('ruby') #=> [7, 3, 5]
|
105
|
+
Issue.where(id: ids).order_by_query([[:id, ids]]).to_a #=> [Issue<id=7>, Issue<id=3>, Issue<id=5>]
|
106
|
+
```
|
107
|
+
|
60
108
|
## How it works
|
61
109
|
|
62
110
|
Internally this gem builds a query that depends on the current record's order values and looks like:
|
@@ -73,9 +121,25 @@ LIMIT 1
|
|
73
121
|
|
74
122
|
Where `x` correspond to `>` / `<` terms, and `y` to `=` terms (for resolving ties), per order criterion.
|
75
123
|
|
76
|
-
A query may then look like this
|
124
|
+
A query may then look like this:
|
125
|
+
|
126
|
+
```sql
|
127
|
+
-- Current post: pinned=true published_at='2014-03-21 15:01:35.064096' id=9
|
128
|
+
SELECT "posts".* FROM "posts" WHERE
|
129
|
+
("posts"."pinned" = 'f' OR
|
130
|
+
"posts"."pinned" = 't' AND (
|
131
|
+
"posts"."published_at" < '2014-03-21 15:01:35.064096' OR
|
132
|
+
"posts"."published_at" = '2014-03-21 15:01:35.064096' AND "posts"."id" < 9))
|
133
|
+
ORDER BY
|
134
|
+
"posts"."pinned"='t' DESC,
|
135
|
+
"posts"."pinned"='f' DESC, "posts"."published_at" DESC, "posts"."id" DESC
|
136
|
+
LIMIT 1
|
137
|
+
```
|
138
|
+
|
139
|
+
A query for the advanced example would look like this:
|
77
140
|
|
78
141
|
```sql
|
142
|
+
-- Current issue: priority='high' (votes - suspicious_votes)=4 updated_at='2014-03-19 10:23:18.671039' id=9
|
79
143
|
SELECT "issues".* FROM "issues" WHERE
|
80
144
|
("issues"."priority" IN ('medium','low') OR
|
81
145
|
"issues"."priority" = 'high' AND (
|
@@ -129,7 +129,11 @@ module OrderQuery
|
|
129
129
|
end
|
130
130
|
# if current not in result set, do not apply filter
|
131
131
|
return EMPTY_FILTER unless sort_values.present?
|
132
|
-
|
132
|
+
if sort_values.length == 1
|
133
|
+
["#{attr.col_name_sql} = ?", [sort_values]]
|
134
|
+
else
|
135
|
+
["#{attr.col_name_sql} IN (?)", [sort_values]]
|
136
|
+
end
|
133
137
|
else
|
134
138
|
# ord is :asc or :desc
|
135
139
|
op = {before: {asc: '<', desc: '>'}, after: {asc: '>', desc: '<'}}[mode][ord || :asc]
|
data/lib/order_query/version.rb
CHANGED
data/spec/order_query_spec.rb
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
# Simple example
|
4
|
+
class Post < ActiveRecord::Base
|
5
|
+
include OrderQuery
|
6
|
+
order_query :order_list, [
|
7
|
+
[:pinned, [true, false]],
|
8
|
+
[:published_at, :desc],
|
9
|
+
[:id, :desc]
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_post(attr = {})
|
14
|
+
Post.create!({pinned: false, published_at: Time.now}.merge(attr))
|
15
|
+
end
|
16
|
+
|
17
|
+
# Advanced example
|
3
18
|
class Issue < ActiveRecord::Base
|
4
19
|
DISPLAY_ORDER = [
|
5
20
|
[:priority, %w(high medium low)],
|
@@ -17,104 +32,133 @@ class Issue < ActiveRecord::Base
|
|
17
32
|
order_query :id_order_asc, [[:id, :asc]]
|
18
33
|
end
|
19
34
|
|
20
|
-
def create_issue(
|
21
|
-
Issue.create!({priority: 'high', votes: 3, suspicious_votes: 0, updated_at: Time.now}.merge(
|
35
|
+
def create_issue(attr = {})
|
36
|
+
Issue.create!({priority: 'high', votes: 3, suspicious_votes: 0, updated_at: Time.now}.merge(attr))
|
22
37
|
end
|
23
38
|
|
24
39
|
describe 'OrderQuery.order_query' do
|
25
40
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
41
|
+
context 'Issue test model' do
|
42
|
+
t = Time.now
|
43
|
+
datasets = [
|
44
|
+
[
|
45
|
+
['high', 5, 0, t],
|
46
|
+
['high', 5, 1, t],
|
47
|
+
['high', 5, 1, t - 1.day],
|
48
|
+
['medium', 10, 0, t],
|
49
|
+
['medium', 10, 5, t - 12.hours],
|
50
|
+
['low', 30, 0, t + 1.day]
|
51
|
+
],
|
52
|
+
[
|
53
|
+
['high', 5, 0, t],
|
54
|
+
['high', 5, 1, t],
|
55
|
+
['high', 5, 1, t - 1.day],
|
56
|
+
['low', 30, 0, t + 1.day]
|
57
|
+
],
|
58
|
+
[
|
59
|
+
['high', 5, 1, t - 1.day],
|
60
|
+
['low', 30, 0, t + 1.day]
|
61
|
+
],
|
62
|
+
]
|
63
|
+
|
64
|
+
datasets.each_with_index do |ds, i|
|
65
|
+
it "is ordered correctly (test data #{i})" do
|
66
|
+
issues = ds.map do |attr|
|
67
|
+
Issue.new(priority: attr[0], votes: attr[1], suspicious_votes: attr[2], updated_at: attr[3])
|
68
|
+
end
|
69
|
+
issues.reverse_each(&:save!)
|
70
|
+
expect(Issue.display_order.to_a).to eq(issues)
|
71
|
+
issues.each_slice(2) do |prev, cur|
|
72
|
+
cur ||= issues.first
|
73
|
+
expect(prev.display_order.next).to eq(cur)
|
74
|
+
expect(cur.display_order.previous).to eq(prev)
|
75
|
+
expect(cur.display_order.scope.count).to eq(Issue.count)
|
76
|
+
expect(cur.display_order.before.count + 1 + cur.display_order.after.count).to eq(cur.display_order.count)
|
77
|
+
|
78
|
+
expect(cur.display_order.before.to_a.reverse + [cur] + cur.display_order.after.to_a).to eq(Issue.display_order.to_a)
|
79
|
+
end
|
63
80
|
end
|
64
81
|
end
|
65
|
-
end
|
66
82
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
83
|
+
it '#next returns nil when there is only 1 record' do
|
84
|
+
p = create_issue.display_order
|
85
|
+
expect(p.next).to be_nil
|
86
|
+
expect(p.next(true)).to be_nil
|
87
|
+
end
|
72
88
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
89
|
+
it 'is ordered correctly for order query [[:id, :asc]]' do
|
90
|
+
a = create_issue
|
91
|
+
b = create_issue
|
92
|
+
expect(a.id_order_asc.next).to eq b
|
93
|
+
expect(b.id_order_asc.previous).to eq a
|
94
|
+
expect([a] + a.id_order_asc.after.to_a).to eq(Issue.id_order_asc.to_a)
|
95
|
+
expect(b.id_order_asc.before.reverse.to_a + [b]).to eq(Issue.id_order_asc.to_a)
|
96
|
+
expect(Issue.id_order_asc.count).to eq(2)
|
97
|
+
end
|
82
98
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
99
|
+
it '.order_by_query works on a list of ids' do
|
100
|
+
ids = (1..3).map { create_issue.id }
|
101
|
+
expect(Issue.order_by_query([[:id, ids]])).to have(ids.length).issues
|
102
|
+
end
|
87
103
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
104
|
+
it '.order_by_query preserves previous' do
|
105
|
+
create_issue(active: true)
|
106
|
+
expect(Issue.where(active: false).order_by_query([[:id, :desc]])).to have(0).records
|
107
|
+
expect(Issue.where(active: true).order_by_query([[:id, :desc]])).to have(1).record
|
108
|
+
end
|
93
109
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
110
|
+
it '#relative_order_by_query falls back to scope when order condition is missing self' do
|
111
|
+
a = create_issue(priority: 'medium')
|
112
|
+
b = create_issue(priority: 'high')
|
113
|
+
expect(a.relative_order_by_query(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
|
114
|
+
end
|
99
115
|
|
100
|
-
|
101
|
-
|
102
|
-
|
116
|
+
before do
|
117
|
+
Issue.delete_all
|
118
|
+
end
|
103
119
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
120
|
+
before :all do
|
121
|
+
ActiveRecord::Schema.define do
|
122
|
+
self.verbose = false
|
123
|
+
|
124
|
+
create_table :issues do |t|
|
125
|
+
t.column :priority, :string
|
126
|
+
t.column :votes, :integer
|
127
|
+
t.column :suspicious_votes, :integer
|
128
|
+
t.column :announced_at, :datetime
|
129
|
+
t.column :updated_at, :datetime
|
130
|
+
t.column :active, :boolen, null: false, default: true
|
131
|
+
end
|
115
132
|
end
|
133
|
+
|
134
|
+
Issue.reset_column_information
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'Post test model' do
|
139
|
+
it '#next works' do
|
140
|
+
p1 = create_post(pinned: true)
|
141
|
+
o1 = p1.order_list
|
142
|
+
expect(o1.next).to be_nil
|
143
|
+
expect(o1.next(true)).to be_nil
|
144
|
+
p2 = create_post(pinned: false)
|
145
|
+
o2 = p2.order_list
|
146
|
+
expect(o1.next(false)).to eq(p2)
|
147
|
+
expect(o2.next(false)).to be_nil
|
148
|
+
expect(o2.next(true)).to eq(p1)
|
116
149
|
end
|
117
150
|
|
118
|
-
|
151
|
+
before do
|
152
|
+
Post.delete_all
|
153
|
+
end
|
154
|
+
before :all do
|
155
|
+
ActiveRecord::Schema.define do
|
156
|
+
self.verbose = false
|
157
|
+
create_table :posts do |t|
|
158
|
+
t.boolean :pinned
|
159
|
+
t.datetime :published_at
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
119
163
|
end
|
120
164
|
end
|
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.1.
|
4
|
+
version: 0.1.1
|
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-03-
|
11
|
+
date: 2014-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -42,36 +42,37 @@ dependencies:
|
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '2.14'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '2.14'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '10.2'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
69
|
-
description:
|
68
|
+
version: '10.2'
|
69
|
+
description: Find next / previous Active Record(s) in one efficient query
|
70
70
|
email: glex.spb@gmail.com
|
71
71
|
executables: []
|
72
72
|
extensions: []
|
73
73
|
extra_rdoc_files: []
|
74
74
|
files:
|
75
|
+
- CHANGES.md
|
75
76
|
- Gemfile
|
76
77
|
- MIT-LICENSE
|
77
78
|
- README.md
|
@@ -86,7 +87,8 @@ files:
|
|
86
87
|
homepage: https://github.com/glebm/order_query
|
87
88
|
licenses:
|
88
89
|
- MIT
|
89
|
-
metadata:
|
90
|
+
metadata:
|
91
|
+
issue_tracker: https://github.com/glebm/order_query
|
90
92
|
post_install_message:
|
91
93
|
rdoc_options: []
|
92
94
|
require_paths:
|
@@ -106,7 +108,7 @@ rubyforge_project:
|
|
106
108
|
rubygems_version: 2.2.0
|
107
109
|
signing_key:
|
108
110
|
specification_version: 4
|
109
|
-
summary: Find next / previous
|
111
|
+
summary: Find next / previous Active Record(s) in one query
|
110
112
|
test_files:
|
111
113
|
- spec/order_query_spec.rb
|
112
114
|
- spec/spec_helper.rb
|