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