order_query 0.1.1 → 0.1.2
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 +6 -1
- data/Gemfile +1 -0
- data/README.md +14 -10
- data/lib/order_query/order_condition.rb +29 -6
- data/lib/order_query/order_space.rb +48 -18
- data/lib/order_query/relative_order.rb +32 -100
- data/lib/order_query/version.rb +1 -1
- data/lib/order_query/where_builder.rb +116 -0
- data/lib/order_query.rb +13 -13
- data/spec/order_query_spec.rb +134 -108
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65a8e133a25873debf2d301e567ebd385e173aed
|
4
|
+
data.tar.gz: ba20134c27cc4b03deb1f76c382c9a6a0abd9b0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f642f0b9d674d7cdd2b1bc7cae39831cc059baf574787640ddeb1d5170059982e4ff9a93629b4c2a57b897991af15d83940a29ce80b14d09b971b88ac51a4719
|
7
|
+
data.tar.gz: de5fccc4d3e103bd32f2237d5a89d2a70d9df5799e4f205100adf8026200af588214704a6bc92a0ff979017405fac95914c6b758fb6df23ecd61a3a69ad85714
|
data/CHANGES.md
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
+
## 0.1.2
|
2
|
+
|
3
|
+
* Wrap top-level `OR` with a redundant `AND` for [performance reasons](https://github.com/glebm/order_query/issues/3).
|
4
|
+
* Remove redundant parens from the query
|
5
|
+
|
1
6
|
## 0.1.1
|
2
7
|
|
3
|
-
* `#next(true)` and `#previous(true)` return nil if there is only one record in total.
|
8
|
+
* `#next(true)` and `#previous(true)` return `nil` if there is only one record in total.
|
4
9
|
|
5
10
|
## 0.1.0
|
6
11
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -16,7 +16,7 @@ This is slow. Here is where `order_query` comes in!
|
|
16
16
|
Add to Gemfile:
|
17
17
|
|
18
18
|
```ruby
|
19
|
-
gem 'order_query', '~> 0.1.
|
19
|
+
gem 'order_query', '~> 0.1.2'
|
20
20
|
```
|
21
21
|
|
22
22
|
## Usage
|
@@ -43,7 +43,7 @@ Post.order_list #=> ActiveRecord::Relation<...>
|
|
43
43
|
Post.reverse_order_list #=> ActiveRecord::Relation<...>
|
44
44
|
```
|
45
45
|
|
46
|
-
###
|
46
|
+
### Records relative to a given one
|
47
47
|
|
48
48
|
`order_query` also adds an instance method for querying relative to the record:
|
49
49
|
|
@@ -62,7 +62,7 @@ p.after #=> ActiveRecord::Relation<...>
|
|
62
62
|
|
63
63
|
### Advanced options
|
64
64
|
|
65
|
-
|
65
|
+
Pass arrays and custom sql as order conditions:
|
66
66
|
|
67
67
|
```ruby
|
68
68
|
class Issue < ActiveRecord::Base
|
@@ -85,10 +85,9 @@ class Issue < ActiveRecord::Base
|
|
85
85
|
end
|
86
86
|
```
|
87
87
|
|
88
|
-
### Dynamic
|
88
|
+
### Dynamic order conditions
|
89
89
|
|
90
|
-
|
91
|
-
These methods can be called directly directly with the order criteria:
|
90
|
+
To query with dynamic order conditions use `Model.order_by_query` and `Model#relative_order_by_query`:
|
92
91
|
|
93
92
|
```ruby
|
94
93
|
Issue.order_by_query([[:id, :desc]]) #=> ActiveRecord::Relation<...>
|
@@ -97,12 +96,11 @@ Issue.find(31).relative_order_by_query([[:id, :desc]]).next #=> Issue<...>
|
|
97
96
|
Issue.find(31).relative_order_by_query(Issue.visible, [[:id, :desc]]).next #=> Issue<...>
|
98
97
|
```
|
99
98
|
|
100
|
-
This is especially helpful if the order criteria is dynamic, so `order_query` cannot be used to define them beforehand.
|
101
99
|
For example, consider ordering by a list of ids returned from an elasticsearh query:
|
102
100
|
|
103
101
|
```ruby
|
104
102
|
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
|
103
|
+
Issue.where(id: ids).order_by_query([[:id, ids]]).first(2).to_a #=> [Issue<id=7>, Issue<id=3>]
|
106
104
|
```
|
107
105
|
|
108
106
|
## How it works
|
@@ -131,8 +129,9 @@ SELECT "posts".* FROM "posts" WHERE
|
|
131
129
|
"posts"."published_at" < '2014-03-21 15:01:35.064096' OR
|
132
130
|
"posts"."published_at" = '2014-03-21 15:01:35.064096' AND "posts"."id" < 9))
|
133
131
|
ORDER BY
|
134
|
-
"posts"."pinned"='t' DESC,
|
135
|
-
"posts"."
|
132
|
+
"posts"."pinned"='t' DESC, "posts"."pinned"='f' DESC,
|
133
|
+
"posts"."published_at" DESC,
|
134
|
+
"posts"."id" DESC
|
136
135
|
LIMIT 1
|
137
136
|
```
|
138
137
|
|
@@ -158,6 +157,11 @@ ORDER BY
|
|
158
157
|
LIMIT 1
|
159
158
|
```
|
160
159
|
|
160
|
+
The top-level `x0 OR ..` clause is actually wrapped with `x0' AND (x0 OR ...)`, where *x0'* is a non-strict condition,
|
161
|
+
for [performance reasons](https://github.com/glebm/order_query/issues/3).
|
162
|
+
|
163
|
+
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).
|
164
|
+
|
161
165
|
This project uses MIT license.
|
162
166
|
|
163
167
|
|
@@ -2,12 +2,12 @@ module OrderQuery
|
|
2
2
|
class OrderCondition
|
3
3
|
attr_reader :name, :order, :order_order, :options, :scope
|
4
4
|
|
5
|
-
def initialize(scope,
|
6
|
-
|
7
|
-
@options =
|
8
|
-
@name =
|
9
|
-
@order =
|
10
|
-
@order_order =
|
5
|
+
def initialize(scope, spec)
|
6
|
+
spec = spec.dup
|
7
|
+
@options = spec.extract_options!
|
8
|
+
@name = spec[0]
|
9
|
+
@order = spec[1] || :asc
|
10
|
+
@order_order = spec[2] || :desc
|
11
11
|
@scope = scope
|
12
12
|
@unique = @options.key?(:unique) ? !!@options[:unique] : (name.to_s == scope.primary_key)
|
13
13
|
end
|
@@ -16,6 +16,29 @@ module OrderQuery
|
|
16
16
|
@unique
|
17
17
|
end
|
18
18
|
|
19
|
+
def ray?
|
20
|
+
!order.is_a?(Array)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [Object] value
|
24
|
+
# @param [:before, :after] mode
|
25
|
+
# @return [Array] valid order values before / after passed (depending on the mode)
|
26
|
+
def values_around(value, mode, strict = true)
|
27
|
+
ord = order
|
28
|
+
pos = ord.index(value)
|
29
|
+
if pos
|
30
|
+
dir = order_order
|
31
|
+
if mode == :after && dir == :desc || mode == :before && dir == :asc
|
32
|
+
ord.from pos + (strict ? 1 : 0)
|
33
|
+
else
|
34
|
+
ord.first pos + (strict ? 0 : 1)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
# default to all if current is not in sort order values
|
38
|
+
ord
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
19
42
|
def col_name_sql
|
20
43
|
sql = options[:sql]
|
21
44
|
if sql
|
@@ -1,44 +1,74 @@
|
|
1
1
|
require 'order_query/order_condition'
|
2
2
|
module OrderQuery
|
3
|
+
# Combine order specification with a scope
|
3
4
|
class OrderSpace
|
4
|
-
|
5
|
-
attr_reader :order
|
5
|
+
attr_reader :conditions
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
def initialize(scope,
|
7
|
+
# @param [ActiveRecord::Relation] scope
|
8
|
+
# @param [Array<Array<Symbol,String>>] order_spec
|
9
|
+
def initialize(scope, order_spec)
|
10
10
|
@scope = scope
|
11
|
-
@
|
11
|
+
@conditions = order_spec.map { |spec| OrderCondition.new(scope, spec) }
|
12
12
|
end
|
13
13
|
|
14
|
+
# @return [ActiveRecord::Relation]
|
14
15
|
def scope
|
15
16
|
@scope.order(order_by_sql)
|
16
17
|
end
|
17
18
|
|
19
|
+
# @return [ActiveRecord::Relation]
|
18
20
|
def reverse_scope
|
19
21
|
@scope.order(order_by_reverse_sql)
|
20
22
|
end
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
SORT_DIRECTIONS = [:asc, :desc].freeze
|
25
|
+
|
26
|
+
# @return [String]
|
27
|
+
def sort_direction_sql(direction)
|
28
|
+
if SORT_DIRECTIONS.include?(direction)
|
29
|
+
direction.to_s.upcase.freeze
|
30
|
+
else
|
31
|
+
raise ArgumentError.new("sort direction must be in #{SORT_DIRECTIONS.map(&:inspect).join(', ')}, is #{direction.inspect}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Array<String>]
|
36
|
+
def order_by_sql_clauses
|
37
|
+
conditions.map { |cond|
|
38
|
+
case order_spec = cond.order
|
39
|
+
when Symbol
|
40
|
+
"#{cond.col_name_sql} #{sort_direction_sql order_spec}".freeze
|
41
|
+
when Enumerable
|
42
|
+
order_spec.map { |v|
|
43
|
+
"#{cond.col_name_sql}=#{@scope.connection.quote v} #{cond.order_order.to_s.upcase}"
|
44
|
+
}.join(', ').freeze
|
45
|
+
else
|
46
|
+
raise ArgumentError.new("Invalid order #{order_spec.inspect} (#{cond.inspect})")
|
31
47
|
end
|
32
48
|
}
|
33
49
|
end
|
34
50
|
|
35
|
-
|
51
|
+
# @return [Array<String>]
|
52
|
+
def order_by_reverse_sql_clauses
|
36
53
|
swap = {'DESC' => 'ASC', 'ASC' => 'DESC'}
|
37
|
-
|
54
|
+
order_by_sql_clauses.map { |s|
|
55
|
+
s.gsub(/DESC|ASC/) { |m| swap[m] }
|
56
|
+
}
|
38
57
|
end
|
39
58
|
|
59
|
+
# @return [String]
|
60
|
+
def order_by_reverse_sql
|
61
|
+
join_order_by_clauses order_by_reverse_sql_clauses
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [String]
|
40
65
|
def order_by_sql
|
41
|
-
|
66
|
+
join_order_by_clauses order_by_sql_clauses
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param [Array<String>] clauses
|
70
|
+
def join_order_by_clauses(clauses)
|
71
|
+
clauses.join(', ').freeze
|
42
72
|
end
|
43
73
|
end
|
44
74
|
end
|
@@ -1,50 +1,67 @@
|
|
1
1
|
require 'order_query/order_space'
|
2
|
+
require 'order_query/where_builder'
|
3
|
+
|
2
4
|
module OrderQuery
|
3
5
|
|
6
|
+
# Search around a record in a scope
|
4
7
|
class RelativeOrder
|
5
|
-
attr_reader :record, :
|
8
|
+
attr_reader :record, :order
|
9
|
+
delegate :scope, :reverse_scope, to: :order
|
6
10
|
|
7
|
-
|
11
|
+
# @param [ActiveRecord::Base] record
|
12
|
+
# @param [OrderQuery::OrderSpace] order_space
|
13
|
+
def initialize(record, order_space)
|
8
14
|
@record = record
|
9
|
-
@
|
10
|
-
@
|
15
|
+
@order = order_space
|
16
|
+
@query_builder = WhereBuilder.new record, order_space
|
11
17
|
end
|
12
18
|
|
19
|
+
# @return [ActiveRecord::Base]
|
13
20
|
def first
|
14
|
-
|
21
|
+
scope.first
|
15
22
|
end
|
16
23
|
|
24
|
+
# @return [ActiveRecord::Base]
|
17
25
|
def last
|
18
|
-
|
26
|
+
reverse_scope.first
|
19
27
|
end
|
20
28
|
|
29
|
+
# @return [Integer]
|
21
30
|
def count
|
22
31
|
@total ||= scope.count
|
23
32
|
end
|
24
33
|
|
34
|
+
# @return [Integer]
|
25
35
|
def position
|
26
36
|
count - after.count
|
27
37
|
end
|
28
38
|
|
39
|
+
# @params [true, false] loop if true, consider last and first as adjacent (unless they are equal)
|
40
|
+
# @return [ActiveRecord::Base]
|
29
41
|
def next(loop = true)
|
30
|
-
|
42
|
+
unless_record_eq after.first || (first if loop)
|
31
43
|
end
|
32
44
|
|
45
|
+
# @return [ActiveRecord::Base]
|
33
46
|
def previous(loop = true)
|
34
|
-
|
47
|
+
unless_record_eq before.first || (last if loop)
|
35
48
|
end
|
36
49
|
|
50
|
+
# @return [ActiveRecord::Relation]
|
37
51
|
def after
|
38
52
|
records :after
|
39
53
|
end
|
40
54
|
|
55
|
+
# @return [ActiveRecord::Relation]
|
41
56
|
def before
|
42
57
|
records :before
|
43
58
|
end
|
44
59
|
|
45
|
-
|
46
|
-
|
47
|
-
|
60
|
+
# @param [:before, :after] direction
|
61
|
+
# @return [ActiveRecord::Relation]
|
62
|
+
def records(direction)
|
63
|
+
scope = (direction == :after ? order.scope : order.reverse_scope)
|
64
|
+
query, query_args = @query_builder.build_query(direction)
|
48
65
|
if query.present?
|
49
66
|
scope.where(query, *query_args)
|
50
67
|
else
|
@@ -54,95 +71,10 @@ module OrderQuery
|
|
54
71
|
|
55
72
|
protected
|
56
73
|
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
if sort_values.length == 1
|
133
|
-
["#{attr.col_name_sql} = ?", [sort_values]]
|
134
|
-
else
|
135
|
-
["#{attr.col_name_sql} IN (?)", [sort_values]]
|
136
|
-
end
|
137
|
-
else
|
138
|
-
# ord is :asc or :desc
|
139
|
-
op = {before: {asc: '<', desc: '>'}, after: {asc: '>', desc: '<'}}[mode][ord || :asc]
|
140
|
-
["#{attr.col_name_sql} #{op} ?", [value]]
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
def attr_value(attr)
|
145
|
-
record.send attr.name
|
74
|
+
# @param [ActiveRecord::Base] rec
|
75
|
+
# @return [ActiveRecord::Base, nil] rec unless rec == @record
|
76
|
+
def unless_record_eq(rec)
|
77
|
+
rec unless rec == @record
|
146
78
|
end
|
147
79
|
end
|
148
80
|
end
|
data/lib/order_query/version.rb
CHANGED
@@ -0,0 +1,116 @@
|
|
1
|
+
module OrderQuery
|
2
|
+
# Build where clause for searching around a record in an order space
|
3
|
+
class WhereBuilder
|
4
|
+
# @return [ActiveRecord::Base]
|
5
|
+
attr_reader :record
|
6
|
+
# @return [OrderQuery::OrderSpace]
|
7
|
+
attr_reader :order
|
8
|
+
|
9
|
+
# @param [ActiveRecord::Base] record
|
10
|
+
# @param [OrderQuery::OrderSpace] order_space
|
11
|
+
def initialize(record, order_space)
|
12
|
+
@order = order_space
|
13
|
+
@record = record
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [:before or :after] mode
|
17
|
+
# @return [query, parameters] conditions that exclude all elements not before / after the current one
|
18
|
+
def build_query(mode)
|
19
|
+
conditions = order.conditions
|
20
|
+
terms = conditions.map { |cond| [where_mode(cond, mode, true), where_eq(cond)] }
|
21
|
+
query = group_operators terms
|
22
|
+
# Wrap top level OR clause for performance, see https://github.com/glebm/order_query/issues/3
|
23
|
+
if self.class.wrap_top_level_or && !terms[0].include?(EMPTY_FILTER)
|
24
|
+
join_terms 'AND'.freeze,
|
25
|
+
where_mode(conditions.first, mode, false),
|
26
|
+
["(#{query[0]})", query[1]]
|
27
|
+
else
|
28
|
+
query
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Join conditions with operators and parenthesis
|
33
|
+
# @param [Array] term_pairs of query terms [[x0, y0], [x1, y1], ...],
|
34
|
+
# xi, yi are pairs of [query, parameters]
|
35
|
+
# @return [query, parameters]
|
36
|
+
# x0 OR
|
37
|
+
# y0 AND (x1 OR
|
38
|
+
# y1 AND (x2 OR
|
39
|
+
# y2 AND x3))
|
40
|
+
#
|
41
|
+
# Since x matches order criteria with values that come before / after the current record,
|
42
|
+
# and y matches order criteria with values equal to the current record's value (for resolving ties),
|
43
|
+
# the resulting condition matches just the elements that come before / after the record
|
44
|
+
def group_operators(term_pairs)
|
45
|
+
# create "x OR y" string
|
46
|
+
disjunctive = join_terms 'OR'.freeze, *term_pairs[0]
|
47
|
+
rest = term_pairs.from(1)
|
48
|
+
if rest.present?
|
49
|
+
# nest the remaining pairs recursively, appending them with " AND "
|
50
|
+
rest_grouped = group_operators rest
|
51
|
+
rest_grouped[0] = "(#{rest_grouped[0]})" unless rest.length == 1
|
52
|
+
join_terms 'AND'.freeze, disjunctive, rest_grouped
|
53
|
+
else
|
54
|
+
disjunctive
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# joins terms with an operator
|
59
|
+
# @return [query, parameters]
|
60
|
+
def join_terms(op, *terms)
|
61
|
+
[terms.map { |t| t.first.presence }.compact.join(" #{op} "),
|
62
|
+
terms.map(&:second).reduce(:+) || []]
|
63
|
+
end
|
64
|
+
|
65
|
+
EMPTY_FILTER = [''.freeze, []]
|
66
|
+
|
67
|
+
# @return [query, params] Unless order attribute is unique, such as id, return ['WHERE value = ?', current value].
|
68
|
+
def where_eq(cond)
|
69
|
+
if cond.unique?
|
70
|
+
EMPTY_FILTER
|
71
|
+
else
|
72
|
+
[%Q(#{cond.col_name_sql} = ?).freeze, [attr_value(cond)]]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def where_ray(cond, from, mode, strict = true)
|
77
|
+
ops = %w(< >)
|
78
|
+
ops = ops.reverse if mode == :after
|
79
|
+
op = {asc: ops[0], desc: ops[1]}[cond.order || :asc]
|
80
|
+
["#{cond.col_name_sql} #{op}#{'=' unless strict} ?".freeze, [from]]
|
81
|
+
end
|
82
|
+
|
83
|
+
def where_in(cond, values)
|
84
|
+
case values.length
|
85
|
+
when 0
|
86
|
+
EMPTY_FILTER
|
87
|
+
when 1
|
88
|
+
["#{cond.col_name_sql} = ?".freeze, [values]]
|
89
|
+
else
|
90
|
+
["#{cond.col_name_sql} IN (?)".freeze, [values]]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param [:before or :after] mode
|
95
|
+
# @return [query, params] return query conditions for attribute values before / after the current one
|
96
|
+
def where_mode(cond, mode, strict = true)
|
97
|
+
value = attr_value cond
|
98
|
+
if cond.ray?
|
99
|
+
where_ray cond, value, mode, strict
|
100
|
+
else
|
101
|
+
# ord is an array of sort values, ordered first to last
|
102
|
+
# if current not in result set, do not apply filter
|
103
|
+
where_in cond, cond.values_around(value, mode, strict)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def attr_value(cond)
|
108
|
+
record.send cond.name
|
109
|
+
end
|
110
|
+
|
111
|
+
class << self
|
112
|
+
attr_accessor :wrap_top_level_or
|
113
|
+
end
|
114
|
+
self.wrap_top_level_or = true
|
115
|
+
end
|
116
|
+
end
|
data/lib/order_query.rb
CHANGED
@@ -6,19 +6,20 @@ module OrderQuery
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
|
10
|
-
|
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 }
|
9
|
+
scope :order_by_query, ->(order_spec) { OrderSpace.new(self, order_spec).scope }
|
10
|
+
scope :reverse_order_by_query, ->(order_spec) { OrderSpace.new(self, order_spec).reverse_scope }
|
14
11
|
end
|
15
12
|
|
16
|
-
|
17
|
-
|
13
|
+
# @param [ActiveRecord::Relation] scope
|
14
|
+
# @param [Array<Array<Symbol,String>>] order_spec
|
15
|
+
def relative_order_by_query(scope = self.class.all, order_spec)
|
16
|
+
RelativeOrder.new(self, OrderSpace.new(scope, order_spec))
|
18
17
|
end
|
19
18
|
|
20
19
|
module ClassMethods
|
21
20
|
protected
|
21
|
+
# @param [Symbol] name
|
22
|
+
# @param [Array<Array<Symbol,String>>] order_spec
|
22
23
|
# @example
|
23
24
|
# class Issue
|
24
25
|
# order_query :order_display, [[:created_at, :desc], [:id, :desc]]
|
@@ -26,12 +27,11 @@ module OrderQuery
|
|
26
27
|
#
|
27
28
|
# Issue.order_display #=> <ActiveRecord::Relation#...>
|
28
29
|
# Issue.active.find(31).display_order(Issue.active).next #=> <Issue#...>
|
29
|
-
def order_query(name,
|
30
|
-
scope name, -> { order_by_query(
|
31
|
-
scope :"reverse_#{name}", -> { reverse_order_by_query(
|
32
|
-
define_method
|
33
|
-
scope
|
34
|
-
relative_order_by_query(scope, order)
|
30
|
+
def order_query(name, order_spec)
|
31
|
+
scope name, -> { order_by_query(order_spec) }
|
32
|
+
scope :"reverse_#{name}", -> { reverse_order_by_query(order_spec) }
|
33
|
+
define_method name do |scope = nil|
|
34
|
+
relative_order_by_query scope || self.class.all, order_spec
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
data/spec/order_query_spec.rb
CHANGED
@@ -36,127 +36,153 @@ def create_issue(attr = {})
|
|
36
36
|
Issue.create!({priority: 'high', votes: 3, suspicious_votes: 0, updated_at: Time.now}.merge(attr))
|
37
37
|
end
|
38
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
39
|
+
def with_wrap_top_level(value)
|
40
|
+
builder = OrderQuery::WhereBuilder
|
41
|
+
around do |ex|
|
42
|
+
was = builder.wrap_top_level_or
|
43
|
+
begin
|
44
|
+
builder.wrap_top_level_or = value
|
45
|
+
ex.run
|
46
|
+
ensure
|
47
|
+
builder.wrap_top_level_or = was
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'OrderQuery' do
|
53
|
+
|
54
|
+
[false, true].each do |wrap_top_level_or|
|
55
|
+
context "(wrap_top_level_or: #{wrap_top_level_or})" do
|
56
|
+
with_wrap_top_level wrap_top_level_or
|
57
|
+
|
58
|
+
context 'Issue test model' do
|
59
|
+
t = Time.now
|
60
|
+
datasets = [
|
61
|
+
[
|
62
|
+
['high', 5, 0, t],
|
63
|
+
['high', 5, 1, t],
|
64
|
+
['high', 5, 1, t - 1.day],
|
65
|
+
['medium', 10, 0, t],
|
66
|
+
['medium', 10, 5, t - 12.hours],
|
67
|
+
['low', 30, 0, t + 1.day]
|
68
|
+
],
|
69
|
+
[
|
70
|
+
['high', 5, 0, t],
|
71
|
+
['high', 5, 1, t],
|
72
|
+
['high', 5, 1, t - 1.day],
|
73
|
+
['low', 30, 0, t + 1.day]
|
74
|
+
],
|
75
|
+
[
|
76
|
+
['high', 5, 1, t - 1.day],
|
77
|
+
['low', 30, 0, t + 1.day]
|
78
|
+
],
|
79
|
+
]
|
80
|
+
|
81
|
+
datasets.each_with_index do |ds, i|
|
82
|
+
it "is ordered correctly (test data #{i})" do
|
83
|
+
issues = ds.map do |attr|
|
84
|
+
Issue.new(priority: attr[0], votes: attr[1], suspicious_votes: attr[2], updated_at: attr[3])
|
85
|
+
end
|
86
|
+
issues.reverse_each(&:save!)
|
87
|
+
expect(Issue.display_order.to_a).to eq(issues)
|
88
|
+
issues.each_slice(2) do |prev, cur|
|
89
|
+
cur ||= issues.first
|
90
|
+
expect(prev.display_order.next).to eq(cur)
|
91
|
+
expect(cur.display_order.previous).to eq(prev)
|
92
|
+
expect(cur.display_order.scope.count).to eq(Issue.count)
|
93
|
+
expect(cur.display_order.before.count + 1 + cur.display_order.after.count).to eq(cur.display_order.count)
|
94
|
+
|
95
|
+
expect(cur.display_order.before.to_a.reverse + [cur] + cur.display_order.after.to_a).to eq(Issue.display_order.to_a)
|
96
|
+
end
|
97
|
+
end
|
68
98
|
end
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
expect(
|
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)
|
99
|
+
|
100
|
+
it '#next returns nil when there is only 1 record' do
|
101
|
+
p = create_issue.display_order
|
102
|
+
expect(p.next).to be_nil
|
103
|
+
expect(p.next(true)).to be_nil
|
79
104
|
end
|
80
|
-
end
|
81
|
-
end
|
82
105
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
106
|
+
it 'is ordered correctly for order query [[:id, :asc]]' do
|
107
|
+
a = create_issue
|
108
|
+
b = create_issue
|
109
|
+
expect(a.id_order_asc.next).to eq b
|
110
|
+
expect(b.id_order_asc.previous).to eq a
|
111
|
+
expect([a] + a.id_order_asc.after.to_a).to eq(Issue.id_order_asc.to_a)
|
112
|
+
expect(b.id_order_asc.before.reverse.to_a + [b]).to eq(Issue.id_order_asc.to_a)
|
113
|
+
expect(Issue.id_order_asc.count).to eq(2)
|
114
|
+
end
|
88
115
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
116
|
+
it '.order_by_query works on a list of ids' do
|
117
|
+
ids = (1..3).map { create_issue.id }
|
118
|
+
expect(Issue.order_by_query([[:id, ids]]).size).to eq ids.length
|
119
|
+
end
|
98
120
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
121
|
+
it '.order_by_query preserves previous' do
|
122
|
+
create_issue(active: true)
|
123
|
+
expect(Issue.where(active: false).order_by_query([[:id, :desc]])).to be_empty
|
124
|
+
expect(Issue.where(active: true).order_by_query([[:id, :desc]]).size).to eq 1
|
125
|
+
end
|
103
126
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
127
|
+
it '#relative_order_by_query falls back to scope when order condition is missing self' do
|
128
|
+
a = create_issue(priority: 'medium')
|
129
|
+
b = create_issue(priority: 'high')
|
130
|
+
expect(a.relative_order_by_query(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
|
131
|
+
end
|
109
132
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
expect(a.relative_order_by_query(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
|
114
|
-
end
|
133
|
+
before do
|
134
|
+
Issue.delete_all
|
135
|
+
end
|
115
136
|
|
116
|
-
|
117
|
-
|
118
|
-
|
137
|
+
before :all do
|
138
|
+
ActiveRecord::Schema.define do
|
139
|
+
self.verbose = false
|
140
|
+
|
141
|
+
create_table :issues do |t|
|
142
|
+
t.column :priority, :string
|
143
|
+
t.column :votes, :integer
|
144
|
+
t.column :suspicious_votes, :integer
|
145
|
+
t.column :announced_at, :datetime
|
146
|
+
t.column :updated_at, :datetime
|
147
|
+
t.column :active, :boolen, null: false, default: true
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
Issue.reset_column_information
|
152
|
+
end
|
119
153
|
|
120
|
-
|
121
|
-
|
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
|
154
|
+
after :all do
|
155
|
+
ActiveRecord::Migration.drop_table :issues
|
131
156
|
end
|
132
157
|
end
|
133
158
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
expect(o1.next(false)).to eq(p2)
|
147
|
-
expect(o2.next(false)).to be_nil
|
148
|
-
expect(o2.next(true)).to eq(p1)
|
149
|
-
end
|
159
|
+
context 'Post test model' do
|
160
|
+
it '#next works' do
|
161
|
+
p1 = create_post(pinned: true)
|
162
|
+
o1 = p1.order_list
|
163
|
+
expect(o1.next).to be_nil
|
164
|
+
expect(o1.next(true)).to be_nil
|
165
|
+
p2 = create_post(pinned: false)
|
166
|
+
o2 = p2.order_list
|
167
|
+
expect(o1.next(false)).to eq(p2)
|
168
|
+
expect(o2.next(false)).to be_nil
|
169
|
+
expect(o2.next(true)).to eq(p1)
|
170
|
+
end
|
150
171
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
172
|
+
before do
|
173
|
+
Post.delete_all
|
174
|
+
end
|
175
|
+
before :all do
|
176
|
+
ActiveRecord::Schema.define do
|
177
|
+
self.verbose = false
|
178
|
+
create_table :posts do |t|
|
179
|
+
t.boolean :pinned
|
180
|
+
t.datetime :published_at
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
after :all do
|
185
|
+
ActiveRecord::Migration.drop_table :posts
|
160
186
|
end
|
161
187
|
end
|
162
188
|
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.2
|
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-
|
11
|
+
date: 2014-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '3.0'
|
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: '3.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -82,6 +82,7 @@ files:
|
|
82
82
|
- lib/order_query/order_space.rb
|
83
83
|
- lib/order_query/relative_order.rb
|
84
84
|
- lib/order_query/version.rb
|
85
|
+
- lib/order_query/where_builder.rb
|
85
86
|
- spec/order_query_spec.rb
|
86
87
|
- spec/spec_helper.rb
|
87
88
|
homepage: https://github.com/glebm/order_query
|
@@ -105,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
106
|
version: '0'
|
106
107
|
requirements: []
|
107
108
|
rubyforge_project:
|
108
|
-
rubygems_version: 2.
|
109
|
+
rubygems_version: 2.4.1
|
109
110
|
signing_key:
|
110
111
|
specification_version: 4
|
111
112
|
summary: Find next / previous Active Record(s) in one query
|