order_query 0.1.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 +7 -0
- data/Gemfile +22 -0
- data/MIT-LICENSE +20 -0
- data/README.md +106 -0
- data/Rakefile +10 -0
- data/lib/order_query/order_condition.rb +29 -0
- data/lib/order_query/order_space.rb +44 -0
- data/lib/order_query/relative_order.rb +144 -0
- data/lib/order_query/version.rb +3 -0
- data/lib/order_query.rb +38 -0
- data/spec/order_query_spec.rb +120 -0
- data/spec/spec_helper.rb +16 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6d9ac479b13db9c6531719b51bca15d8079b2b37
|
4
|
+
data.tar.gz: 1c9e90573f1cf98650bdf2d2de5d9969e10ce5fb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 38e902e5c4804d578d6b4817e9b428c4eb295f3fa87d6cf11efae6592630594f7df2d036c1242766ded5f5d58c703a7f01689d60332be0ef5c0bfe2da375f5e3
|
7
|
+
data.tar.gz: c7830348c5d536abd766fbd9d9834f3359a09f110c62bef57a69a74d4d7ee23f0b08df55650a1abf00247ea675d099d0afe6f5f2b93f4938a1544d66750451cf
|
data/Gemfile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
group :test, :development do
|
6
|
+
gem 'coveralls', require: false
|
7
|
+
end
|
8
|
+
|
9
|
+
platform :mri, :rbx do
|
10
|
+
# version locked because of rbx issue, see https://github.com/travis-ci/travis-ci/issues/2006#issuecomment-36275141
|
11
|
+
gem 'sqlite3', '=1.3.8'
|
12
|
+
end
|
13
|
+
|
14
|
+
platform :jruby do
|
15
|
+
gem 'activerecord-jdbcsqlite3-adapter'
|
16
|
+
end
|
17
|
+
|
18
|
+
platform :rbx do
|
19
|
+
gem 'rubysl-singleton', '~> 2.0'
|
20
|
+
gem 'rubysl-optparse', '~> 2.0'
|
21
|
+
gem 'rubysl-ostruct', '~> 2.0'
|
22
|
+
end
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 Gleb Mazovetskiy
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# order_query [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Coverage Status][coveralls-badge]][coveralls]
|
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.
|
5
|
+
|
6
|
+
```ruby
|
7
|
+
gem 'order_query', '~> 0.1.0'
|
8
|
+
```
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
class Issue < ActiveRecord::Base
|
14
|
+
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]
|
22
|
+
]
|
23
|
+
def valid_votes_count
|
24
|
+
votes - suspicious_votes
|
25
|
+
end
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
Order scopes:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
Issue.order_display #=> ActiveRecord::Relation<...>
|
33
|
+
Issue.reverse_order_display #=> ActiveRecord::Relation<...>
|
34
|
+
```
|
35
|
+
|
36
|
+
Relative order:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
# get the order object, scope default: Issue.all
|
40
|
+
p = Issue.find(31).order_display(scope)
|
41
|
+
p.before #=> ActiveRecord::Relation<...>
|
42
|
+
p.previous #=> Issue<...>
|
43
|
+
# pass true to #next and #previous in order to loop onto the the first / last record
|
44
|
+
# will not loop onto itself
|
45
|
+
p.previous(true) #=> Issue<...>
|
46
|
+
p.position #=> 5
|
47
|
+
p.next #=> Issue<...>
|
48
|
+
p.after #=> ActiveRecord::Relation<...>
|
49
|
+
```
|
50
|
+
|
51
|
+
`order_query` defines methods that call `.order_by_query` and `#relative_order_by_query`, also public:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
Issue.order_by_query([[:id, :desc]]) #=> ActiveRecord::Relation<...>
|
55
|
+
Issue.reverse_order_by_query([[:id, :desc]]) #=> ActiveRecord::Relation<...>
|
56
|
+
Issue.find(31).relative_order_by_query([[:id, :desc]]).next #=> Issue<...>
|
57
|
+
Issue.find(31).relative_order_by_query(Issue.visible, [[:id, :desc]]).next #=> Issue<...>
|
58
|
+
```
|
59
|
+
|
60
|
+
## How it works
|
61
|
+
|
62
|
+
Internally this gem builds a query that depends on the current record's order values and looks like:
|
63
|
+
|
64
|
+
```sql
|
65
|
+
SELECT ... WHERE
|
66
|
+
x0 OR
|
67
|
+
y0 AND (x1 OR
|
68
|
+
y1 AND (x2 OR
|
69
|
+
y2 AND ...))
|
70
|
+
ORDER BY ...
|
71
|
+
LIMIT 1
|
72
|
+
```
|
73
|
+
|
74
|
+
Where `x` correspond to `>` / `<` terms, and `y` to `=` terms (for resolving ties), per order criterion.
|
75
|
+
|
76
|
+
A query may then look like this (with `?` for values):
|
77
|
+
|
78
|
+
```sql
|
79
|
+
SELECT "issues".* FROM "issues" WHERE
|
80
|
+
("issues"."priority" IN ('medium','low') OR
|
81
|
+
"issues"."priority" = 'high' AND (
|
82
|
+
(votes - suspicious_votes) < 4 OR
|
83
|
+
(votes - suspicious_votes) = 4 AND (
|
84
|
+
"issues"."updated_at" < '2014-03-19 10:23:18.671039' OR
|
85
|
+
"issues"."updated_at" = '2014-03-19 10:23:18.671039' AND
|
86
|
+
"issues"."id" < 9)))
|
87
|
+
ORDER BY
|
88
|
+
"issues"."priority"='high' DESC,
|
89
|
+
"issues"."priority"='medium' DESC,
|
90
|
+
"issues"."priority"='low' DESC,
|
91
|
+
(votes - suspicious_votes) DESC,
|
92
|
+
"issues"."updated_at" DESC,
|
93
|
+
"issues"."id" DESC
|
94
|
+
LIMIT 1
|
95
|
+
```
|
96
|
+
|
97
|
+
This project uses MIT license.
|
98
|
+
|
99
|
+
|
100
|
+
[travis]: http://travis-ci.org/glebm/order_query
|
101
|
+
[travis-badge]: http://img.shields.io/travis/glebm/order_query.svg
|
102
|
+
[gemnasium]: https://gemnasium.com/glebm/order_query
|
103
|
+
[codeclimate]: https://codeclimate.com/github/glebm/order_query
|
104
|
+
[codeclimate-badge]: http://img.shields.io/codeclimate/github/glebm/order_query.svg
|
105
|
+
[coveralls]: https://coveralls.io/r/glebm/order_query
|
106
|
+
[coveralls-badge]: http://img.shields.io/coveralls/glebm/order_query.svg
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module OrderQuery
|
2
|
+
class OrderCondition
|
3
|
+
attr_reader :name, :order, :order_order, :options, :scope
|
4
|
+
|
5
|
+
def initialize(scope, line)
|
6
|
+
line = line.dup
|
7
|
+
@options = line.extract_options!
|
8
|
+
@name = line[0]
|
9
|
+
@order = line[1] || :asc
|
10
|
+
@order_order = line[2] || :desc
|
11
|
+
@scope = scope
|
12
|
+
@unique = @options.key?(:unique) ? !!@options[:unique] : (name.to_s == scope.primary_key)
|
13
|
+
end
|
14
|
+
|
15
|
+
def unique?
|
16
|
+
@unique
|
17
|
+
end
|
18
|
+
|
19
|
+
def col_name_sql
|
20
|
+
sql = options[:sql]
|
21
|
+
if sql
|
22
|
+
sql = sql.call if sql.respond_to?(:call)
|
23
|
+
sql
|
24
|
+
else
|
25
|
+
scope.connection.quote_table_name(scope.table_name) + '.' + scope.connection.quote_column_name(name)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'order_query/order_condition'
|
2
|
+
module OrderQuery
|
3
|
+
class OrderSpace
|
4
|
+
include Enumerable
|
5
|
+
attr_reader :order
|
6
|
+
|
7
|
+
delegate :each, :length, :size, to: :@order
|
8
|
+
|
9
|
+
def initialize(scope, order)
|
10
|
+
@scope = scope
|
11
|
+
@order = order.map { |line| OrderCondition.new(scope, line) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def scope
|
15
|
+
@scope.order(order_by_sql)
|
16
|
+
end
|
17
|
+
|
18
|
+
def reverse_scope
|
19
|
+
@scope.order(order_by_reverse_sql)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_order_by_sql
|
23
|
+
@order.map { |spec|
|
24
|
+
ord = spec.order
|
25
|
+
if ord == :asc || ord == :desc
|
26
|
+
"#{spec.col_name_sql} #{ord.to_s.upcase}"
|
27
|
+
elsif ord.respond_to?(:map)
|
28
|
+
ord.map { |v| "#{spec.col_name_sql}=#{@scope.connection.quote v} #{spec.order_order.to_s.upcase}" } * ', '
|
29
|
+
else
|
30
|
+
raise "Unknown order #{spec.order.inspect} (#{spec.inspect})"
|
31
|
+
end
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def order_by_reverse_sql
|
36
|
+
swap = {'DESC' => 'ASC', 'ASC' => 'DESC'}
|
37
|
+
to_order_by_sql.map { |s| s.gsub(/DESC|ASC/) { |m| swap[m] } } * ', '
|
38
|
+
end
|
39
|
+
|
40
|
+
def order_by_sql
|
41
|
+
to_order_by_sql * ', '
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'order_query/order_space'
|
2
|
+
module OrderQuery
|
3
|
+
|
4
|
+
class RelativeOrder
|
5
|
+
attr_reader :record, :scope, :order, :values, :options
|
6
|
+
|
7
|
+
def initialize(record, scope, order)
|
8
|
+
@record = record
|
9
|
+
@scope = scope
|
10
|
+
@order = order.is_a?(OrderSpace) ? order : OrderSpace.new(scope, order)
|
11
|
+
end
|
12
|
+
|
13
|
+
def first
|
14
|
+
order.scope.first
|
15
|
+
end
|
16
|
+
|
17
|
+
def last
|
18
|
+
order.scope.last
|
19
|
+
end
|
20
|
+
|
21
|
+
def count
|
22
|
+
@total ||= scope.count
|
23
|
+
end
|
24
|
+
|
25
|
+
def position
|
26
|
+
count - after.count
|
27
|
+
end
|
28
|
+
|
29
|
+
def next(loop = true)
|
30
|
+
record_unless_current after.first || (first if loop)
|
31
|
+
end
|
32
|
+
|
33
|
+
def previous(loop = true)
|
34
|
+
record_unless_current before.first || (last if loop)
|
35
|
+
end
|
36
|
+
|
37
|
+
def after
|
38
|
+
records :after
|
39
|
+
end
|
40
|
+
|
41
|
+
def before
|
42
|
+
records :before
|
43
|
+
end
|
44
|
+
|
45
|
+
def records(mode)
|
46
|
+
scope = (mode == :after ? order.scope : order.reverse_scope)
|
47
|
+
query, query_args = build_query(mode)
|
48
|
+
if query.present?
|
49
|
+
scope.where(query, *query_args)
|
50
|
+
else
|
51
|
+
scope
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def record_unless_current(record)
|
58
|
+
record unless record == @record
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param [:before or :after] mode
|
62
|
+
# @return [query, parameters] conditions that exclude all elements not before / after the current one
|
63
|
+
def build_query(mode)
|
64
|
+
group_operators order.map { |term| [where_mode(term, mode), where_eq(term)] }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Join conditions with operators and parenthesis
|
68
|
+
# @param [Array] term_pairs of query terms [[x0, y0], [x1, y1], ...],
|
69
|
+
# xi, yi are pairs of [query, parameters]
|
70
|
+
# @return [query, parameters]
|
71
|
+
# x0 OR
|
72
|
+
# y0 AND (x1 OR
|
73
|
+
# y1 AND (x2 OR
|
74
|
+
# y2 AND x3))
|
75
|
+
#
|
76
|
+
# Since x matches order criteria with values that come before / after the current record,
|
77
|
+
# and y matches order criteria with values equal to the current record's value (for resolving ties),
|
78
|
+
# the resulting condition matches just the elements that come before / after the record
|
79
|
+
def group_operators(term_pairs)
|
80
|
+
# create "x OR y" string
|
81
|
+
term = join_terms 'OR', *term_pairs[0]
|
82
|
+
rest = term_pairs.from(1)
|
83
|
+
if rest.present?
|
84
|
+
# nest the remaining pairs recursively, appending them with " AND "
|
85
|
+
rest_grouped = group_operators rest
|
86
|
+
rest_grouped[0] = "(#{rest_grouped[0]})" unless rest.length == 1
|
87
|
+
join_terms 'AND', term, rest_grouped
|
88
|
+
else
|
89
|
+
term
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# joins terms with an operator
|
94
|
+
# @return [query, parameters]
|
95
|
+
def join_terms(op, *terms)
|
96
|
+
[terms.map { |t| t.first.presence }.compact.join(" #{op} "),
|
97
|
+
terms.map(&:second).reduce(:+) || []]
|
98
|
+
end
|
99
|
+
|
100
|
+
EMPTY_FILTER = ['', []]
|
101
|
+
|
102
|
+
# @return [query, params] Unless order attribute is unique, such as id, return ['WHERE value = ?', current value].
|
103
|
+
def where_eq(attr)
|
104
|
+
if attr.unique?
|
105
|
+
EMPTY_FILTER
|
106
|
+
else
|
107
|
+
[%Q(#{attr.col_name_sql} = ?), [attr_value(attr)]]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param [:before or :after] mode
|
112
|
+
# @return [query, params] return query conditions for attribute values before / after the current one
|
113
|
+
def where_mode(attr, mode)
|
114
|
+
ord = attr.order
|
115
|
+
value = attr_value attr
|
116
|
+
if ord.is_a?(Array)
|
117
|
+
# ord is an array of sort values, ordered first to last
|
118
|
+
pos = ord.index(value)
|
119
|
+
sort_values = if pos
|
120
|
+
dir = attr.order_order
|
121
|
+
if mode == :after && dir == :desc || mode == :before && dir == :asc
|
122
|
+
ord.from(pos + 1)
|
123
|
+
else
|
124
|
+
ord.first(pos)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
# default to all if current is not in sort order values
|
128
|
+
ord
|
129
|
+
end
|
130
|
+
# if current not in result set, do not apply filter
|
131
|
+
return EMPTY_FILTER unless sort_values.present?
|
132
|
+
["#{attr.col_name_sql} IN (?)", [sort_values]]
|
133
|
+
else
|
134
|
+
# ord is :asc or :desc
|
135
|
+
op = {before: {asc: '<', desc: '>'}, after: {asc: '>', desc: '<'}}[mode][ord || :asc]
|
136
|
+
["#{attr.col_name_sql} #{op} ?", [value]]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def attr_value(attr)
|
141
|
+
record.send attr.name
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/order_query.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_record'
|
3
|
+
require 'order_query/relative_order'
|
4
|
+
|
5
|
+
module OrderQuery
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
# @return [OrderSpace] order definition
|
10
|
+
# @example
|
11
|
+
# Issue.order_query [[:id, :desc]] #=> <ActiveRecord::Relation#...>
|
12
|
+
scope :order_by_query, ->(order) { OrderSpace.new(self, order).scope }
|
13
|
+
scope :reverse_order_by_query, ->(order) { OrderSpace.new(self, order).reverse_scope }
|
14
|
+
end
|
15
|
+
|
16
|
+
def relative_order_by_query(scope = self.class.all, order)
|
17
|
+
RelativeOrder.new(self, scope, order)
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
protected
|
22
|
+
# @example
|
23
|
+
# class Issue
|
24
|
+
# order_query :order_display, [[:created_at, :desc], [:id, :desc]]
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# Issue.order_display #=> <ActiveRecord::Relation#...>
|
28
|
+
# Issue.active.find(31).display_order(Issue.active).next #=> <Issue#...>
|
29
|
+
def order_query(name, order)
|
30
|
+
scope name, -> { order_by_query(order) }
|
31
|
+
scope :"reverse_#{name}", -> { reverse_order_by_query(order) }
|
32
|
+
define_method(name) do |scope = nil|
|
33
|
+
scope ||= self.class.all
|
34
|
+
relative_order_by_query(scope, order)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Issue < ActiveRecord::Base
|
4
|
+
DISPLAY_ORDER = [
|
5
|
+
[:priority, %w(high medium low)],
|
6
|
+
[:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
|
7
|
+
[:updated_at, :desc],
|
8
|
+
[:id, :desc]
|
9
|
+
]
|
10
|
+
|
11
|
+
def valid_votes_count
|
12
|
+
votes - suspicious_votes
|
13
|
+
end
|
14
|
+
|
15
|
+
include OrderQuery
|
16
|
+
order_query :display_order, DISPLAY_ORDER
|
17
|
+
order_query :id_order_asc, [[:id, :asc]]
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_issue(options = {})
|
21
|
+
Issue.create!({priority: 'high', votes: 3, suspicious_votes: 0, updated_at: Time.now}.merge(options))
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'OrderQuery.order_query' do
|
25
|
+
|
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)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
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
|
72
|
+
|
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
|
82
|
+
|
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
|
87
|
+
|
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
|
93
|
+
|
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
|
99
|
+
|
100
|
+
before do
|
101
|
+
Issue.delete_all
|
102
|
+
end
|
103
|
+
|
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
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
Issue.reset_column_information
|
119
|
+
end
|
120
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# Configure Rails Environment
|
3
|
+
ENV['RAILS_ENV'] = ENV['RACK_ENV'] = 'test'
|
4
|
+
unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
|
5
|
+
begin
|
6
|
+
require 'coveralls'
|
7
|
+
Coveralls.wear!
|
8
|
+
rescue LoadError
|
9
|
+
false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
require 'order_query'
|
13
|
+
|
14
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
15
|
+
|
16
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: order_query
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gleb Mazovetskiy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email: glex.spb@gmail.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- Gemfile
|
76
|
+
- MIT-LICENSE
|
77
|
+
- README.md
|
78
|
+
- Rakefile
|
79
|
+
- lib/order_query.rb
|
80
|
+
- lib/order_query/order_condition.rb
|
81
|
+
- lib/order_query/order_space.rb
|
82
|
+
- lib/order_query/relative_order.rb
|
83
|
+
- lib/order_query/version.rb
|
84
|
+
- spec/order_query_spec.rb
|
85
|
+
- spec/spec_helper.rb
|
86
|
+
homepage: https://github.com/glebm/order_query
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.2.0
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Find next / previous record(s) in one query, for ActiveRecord
|
110
|
+
test_files:
|
111
|
+
- spec/order_query_spec.rb
|
112
|
+
- spec/spec_helper.rb
|