order_query 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6d9ac479b13db9c6531719b51bca15d8079b2b37
4
- data.tar.gz: 1c9e90573f1cf98650bdf2d2de5d9969e10ce5fb
3
+ metadata.gz: 2e1364db02e6ba3b3d20bf11d77103dc33a1d287
4
+ data.tar.gz: 70f8f66433ef24caebe56b83a165073ac7377f88
5
5
  SHA512:
6
- metadata.gz: 38e902e5c4804d578d6b4817e9b428c4eb295f3fa87d6cf11efae6592630594f7df2d036c1242766ded5f5d58c703a7f01689d60332be0ef5c0bfe2da375f5e3
7
- data.tar.gz: c7830348c5d536abd766fbd9d9834f3359a09f110c62bef57a69a74d4d7ee23f0b08df55650a1abf00247ea675d099d0afe6f5f2b93f4938a1544d66750451cf
6
+ metadata.gz: 2ea5b0e8beb0268407ce15504ce0d57200c41fe4f059782c220c7c2b84c40205dd12123eb61f67e9b01d1c287b836123424ae6925d585142bea19b0077d2cf78
7
+ data.tar.gz: 3a9dc43bab64321c33f27d2869f895cb36efab27fea662919e61a49d7454863fef8c83ee16a50350b6beb789b51f86936b80636ecac4dfdf1b948a44ce4b0057
data/CHANGES.md ADDED
@@ -0,0 +1,7 @@
1
+ ## 0.1.1
2
+
3
+ * `#next(true)` and `#previous(true)` return nil if there is only one record in total.
4
+
5
+ ## 0.1.0
6
+
7
+ Initial release
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 provides ActiveRecord methods to find items relative to the position of a given one for a particular ordering. These methods are useful for many navigation scenarios, e.g. links to the next / previous search result from the show page in a typical index/search -> show scenario.
4
- order_query generates queries that only use `WHERE`, `ORDER BY`, and `LIMIT`, and *not* `OFFSET`. It only takes 1 query (returning 1 row) to get the record before or after the given one.
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.0'
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 Issue < ActiveRecord::Base
27
+ class Post < ActiveRecord::Base
14
28
  include OrderQuery
15
- order_query :order_display, [
16
- [:priority, %w(high medium low)],
17
- [:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
18
- [:updated_at, :desc],
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
- Issue.order_display #=> ActiveRecord::Relation<...>
33
- Issue.reverse_order_display #=> ActiveRecord::Relation<...>
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: Issue.all
40
- p = Issue.find(31).order_display(scope)
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 #=> Issue<...>
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) #=> Issue<...>
57
+ p.previous(true) #=> Post<...>
46
58
  p.position #=> 5
47
- p.next #=> Issue<...>
59
+ p.next #=> Post<...>
48
60
  p.after #=> ActiveRecord::Relation<...>
49
61
  ```
50
62
 
51
- `order_query` defines methods that call `.order_by_query` and `#relative_order_by_query`, also public:
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 (with `?` for values):
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
- ["#{attr.col_name_sql} IN (?)", [sort_values]]
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]
@@ -1,3 +1,3 @@
1
1
  module OrderQuery
2
- VERSION = '0.1.0'
2
+ VERSION = '0.1.1'
3
3
  end
@@ -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(options = {})
21
- Issue.create!({priority: 'high', votes: 3, suspicious_votes: 0, updated_at: Time.now}.merge(options))
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
- t = Time.now
27
- datasets = [
28
- [
29
- ['high', 5, 0, t],
30
- ['high', 5, 1, t],
31
- ['high', 5, 1, t - 1.day],
32
- ['medium', 10, 0, t],
33
- ['medium', 10, 5, t - 12.hours],
34
- ['low', 30, 0, t + 1.day]
35
- ],
36
- [
37
- ['high', 5, 0, t],
38
- ['high', 5, 1, t],
39
- ['high', 5, 1, t - 1.day],
40
- ['low', 30, 0, t + 1.day]
41
- ],
42
- [
43
- ['high', 5, 1, t - 1.day],
44
- ['low', 30, 0, t + 1.day]
45
- ],
46
- ]
47
-
48
- datasets.each_with_index do |ds, i|
49
- it "is ordered correctly (test data #{i})" do
50
- issues = ds.map do |attr|
51
- Issue.new(priority: attr[0], votes: attr[1], suspicious_votes: attr[2], updated_at: attr[3])
52
- end
53
- issues.reverse_each(&:save!)
54
- expect(Issue.display_order.to_a).to eq(issues)
55
- issues.each_slice(2) do |prev, cur|
56
- cur ||= issues.first
57
- expect(prev.display_order.next).to eq(cur)
58
- expect(cur.display_order.previous).to eq(prev)
59
- expect(cur.display_order.scope.count).to eq(Issue.count)
60
- expect(cur.display_order.before.count + 1 + cur.display_order.after.count).to eq(cur.display_order.count)
61
-
62
- expect(cur.display_order.before.to_a.reverse + [cur] + cur.display_order.after.to_a).to eq(Issue.display_order.to_a)
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
- it '#next returns nil when there is only 1 record' do
68
- p = create_issue.display_order
69
- expect(p.next).to be_nil
70
- expect(p.next(true)).to be_nil
71
- end
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
- it 'is ordered correctly for order query [[:id, :asc]]' do
74
- a = create_issue
75
- b = create_issue
76
- expect(a.id_order_asc.next).to eq b
77
- expect(b.id_order_asc.previous).to eq a
78
- expect([a] + a.id_order_asc.after.to_a).to eq(Issue.id_order_asc.to_a)
79
- expect(b.id_order_asc.before.reverse.to_a + [b]).to eq(Issue.id_order_asc.to_a)
80
- expect(Issue.id_order_asc.count).to eq(2)
81
- end
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
- it '.order_by_query works on a list of ids' do
84
- ids = (1..3).map { create_issue.id }
85
- expect(Issue.order_by_query([[:id, ids]])).to have(ids.length).issues
86
- end
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
- it '.order_by_query preserves previous' do
89
- create_issue(active: true)
90
- expect(Issue.where(active: false).order_by_query([[:id, :desc]])).to have(0).records
91
- expect(Issue.where(active: true).order_by_query([[:id, :desc]])).to have(1).record
92
- end
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
- it '#relative_order_by_query falls back to scope when order condition is missing self' do
95
- a = create_issue(priority: 'medium')
96
- b = create_issue(priority: 'high')
97
- expect(a.relative_order_by_query(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
98
- end
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
- before do
101
- Issue.delete_all
102
- end
116
+ before do
117
+ Issue.delete_all
118
+ end
103
119
 
104
- before :all do
105
- ActiveRecord::Schema.define do
106
- self.verbose = false
107
-
108
- create_table :issues do |t|
109
- t.column :priority, :string
110
- t.column :votes, :integer
111
- t.column :suspicious_votes, :integer
112
- t.column :announced_at, :datetime
113
- t.column :updated_at, :datetime
114
- t.column :active, :boolen, null: false, default: true
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
- Issue.reset_column_information
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.0
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-19 00:00:00.000000000 Z
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: '0'
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: '0'
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: '0'
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: '0'
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 record(s) in one query, for ActiveRecord
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