order_query 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +4 -0
- data/README.md +11 -10
- data/lib/order_query/condition.rb +13 -16
- data/lib/order_query/point.rb +14 -6
- data/lib/order_query/sql/order_by.rb +11 -9
- data/lib/order_query/sql/where.rb +74 -58
- data/lib/order_query/version.rb +1 -1
- data/spec/order_query_spec.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04dd1e3cb69ac7b1cd363629e1449f2eec4bd921
|
4
|
+
data.tar.gz: 1d2533afcfea18fe8a1b06ac63dec068ef59cf8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f708515aa23195a7cb3051fca8848d1382190429ed9f2055cf0d0bc07b7b66b83c8792d5f1c3fdbbe6c7d3df2f62ec0571cc95755baeec621470c6464c838a05
|
7
|
+
data.tar.gz: 7650728e46013f47e129d9a4e939ef531c687bda7315f8326737d628f1b8bee509395a072c079e342d04e2e34640ea9cde5b4f504eed25625ee05082c61f0a3a
|
data/CHANGES.md
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.2.
|
19
|
+
gem 'order_query', '~> 0.2.1'
|
20
20
|
```
|
21
21
|
|
22
22
|
## Usage
|
@@ -27,7 +27,7 @@ Define a list of order conditions with `order_query`:
|
|
27
27
|
class Post < ActiveRecord::Base
|
28
28
|
include OrderQuery
|
29
29
|
order_query :order_for_index, [
|
30
|
-
[:pinned, [true, false]
|
30
|
+
[:pinned, [true, false]],
|
31
31
|
[:published_at, :desc],
|
32
32
|
[:id, :desc]
|
33
33
|
]
|
@@ -37,11 +37,11 @@ end
|
|
37
37
|
An order condition is specified as an attribute name, optionally an ordered list of values, and a sort direction.
|
38
38
|
Additional options are:
|
39
39
|
|
40
|
-
| option | description
|
41
|
-
|
42
|
-
| unique | Unique attribute
|
43
|
-
| complete |
|
44
|
-
| sql | Customize attribute value SQL
|
40
|
+
| option | description |
|
41
|
+
|------------|----------------------------------------------------------------------------|
|
42
|
+
| unique | Unique attribute. Default: `true` for primary key, `false` otherwise. |
|
43
|
+
| complete | Enum attribute contains all the possible values. Default: `true`. |
|
44
|
+
| sql | Customize attribute value SQL |
|
45
45
|
|
46
46
|
### Order scopes
|
47
47
|
|
@@ -77,7 +77,7 @@ class Issue < ActiveRecord::Base
|
|
77
77
|
order_query :order_display, [
|
78
78
|
# Pass an array for attribute order, and an optional sort direction for the array,
|
79
79
|
# default is *:desc*, so that first in the array <=> first in the result
|
80
|
-
[:priority, %w(high medium low), :desc
|
80
|
+
[:priority, %w(high medium low), :desc],
|
81
81
|
# Sort attribute can be a method name, provided you pass :sql for the attribute
|
82
82
|
[:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
|
83
83
|
# Default sort order for non-array attributes is :asc, just like SQL
|
@@ -164,8 +164,9 @@ ORDER BY
|
|
164
164
|
LIMIT 1
|
165
165
|
```
|
166
166
|
|
167
|
-
The top-level `
|
168
|
-
for [performance reasons](https://github.com/glebm/order_query/issues/3).
|
167
|
+
The actual query is a bit different because `order_query` wraps the top-level `OR` with a (redundant) non-strict condition `x0' AND (x0 OR ...)`
|
168
|
+
for [performance reasons](https://github.com/glebm/order_query/issues/3).
|
169
|
+
This can be disabled with `OrderQuery.wrap_top_level_or = false`.
|
169
170
|
|
170
171
|
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).
|
171
172
|
|
@@ -16,18 +16,12 @@ module OrderQuery
|
|
16
16
|
else
|
17
17
|
@order = spec[1] || :asc
|
18
18
|
end
|
19
|
-
@options = options
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@complete = if options.key?(:complete)
|
26
|
-
!!options[:complete]
|
27
|
-
else
|
28
|
-
!@order_enum
|
29
|
-
end
|
30
|
-
|
19
|
+
@options = options.reverse_merge(
|
20
|
+
unique: name.to_s == scope.primary_key,
|
21
|
+
complete: true
|
22
|
+
)
|
23
|
+
@unique = @options[:unique]
|
24
|
+
@complete = @options[:complete]
|
31
25
|
@sql = SQL::Condition.new(self, scope)
|
32
26
|
end
|
33
27
|
|
@@ -40,14 +34,17 @@ module OrderQuery
|
|
40
34
|
end
|
41
35
|
|
42
36
|
# @param [Object] value
|
43
|
-
# @param [:before, :after]
|
44
|
-
# @return [Array] valid order values before / after passed (depending on the
|
45
|
-
|
37
|
+
# @param [:before, :after] side
|
38
|
+
# @return [Array] valid order values before / after passed (depending on the side)
|
39
|
+
# @example for [:difficulty, ['Easy', 'Normal', 'Hard']]:
|
40
|
+
# enum_side('Normal', :after) #=> ['Hard']
|
41
|
+
# enum_side('Normal', :after, false) #=> ['Normal', 'Hard']
|
42
|
+
def enum_side(value, side, strict = true)
|
46
43
|
ord = order_enum
|
47
44
|
pos = ord.index(value)
|
48
45
|
if pos
|
49
46
|
dir = order
|
50
|
-
if
|
47
|
+
if side == :after && dir == :desc || side == :before && dir == :asc
|
51
48
|
ord.from pos + (strict ? 1 : 0)
|
52
49
|
else
|
53
50
|
ord.first pos + (strict ? 0 : 1)
|
data/lib/order_query/point.rb
CHANGED
@@ -48,19 +48,23 @@ module OrderQuery
|
|
48
48
|
|
49
49
|
# @return [ActiveRecord::Relation]
|
50
50
|
def after
|
51
|
-
|
51
|
+
side :after
|
52
52
|
end
|
53
53
|
|
54
54
|
# @return [ActiveRecord::Relation]
|
55
55
|
def before
|
56
|
-
|
56
|
+
side :before
|
57
57
|
end
|
58
58
|
|
59
|
-
# @param [:before, :after]
|
59
|
+
# @param [:before, :after] side
|
60
60
|
# @return [ActiveRecord::Relation]
|
61
|
-
def
|
62
|
-
query, query_args = @where_sql.build(
|
63
|
-
scope =
|
61
|
+
def side(side)
|
62
|
+
query, query_args = @where_sql.build(side)
|
63
|
+
scope = if side == :after
|
64
|
+
space.scope
|
65
|
+
else
|
66
|
+
space.reverse_scope
|
67
|
+
end
|
64
68
|
if query.present?
|
65
69
|
scope.where(query, *query_args)
|
66
70
|
else
|
@@ -68,6 +72,10 @@ module OrderQuery
|
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
75
|
+
def value(cond)
|
76
|
+
record.send(cond.name)
|
77
|
+
end
|
78
|
+
|
71
79
|
protected
|
72
80
|
|
73
81
|
# @param [ActiveRecord::Base] rec
|
@@ -22,15 +22,17 @@ module OrderQuery
|
|
22
22
|
|
23
23
|
# @return [Array<String>]
|
24
24
|
def order_by_sql_clauses
|
25
|
-
space.conditions.map { |cond|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
25
|
+
space.conditions.map { |cond| condition_clause cond }
|
26
|
+
end
|
27
|
+
|
28
|
+
def condition_clause(cond)
|
29
|
+
dir_sql = sort_direction_sql cond.order
|
30
|
+
col_sql = cond.sql.column_name
|
31
|
+
if cond.order_enum
|
32
|
+
cond.order_enum.map { |v| "#{col_sql}=#{cond.sql.quote v} #{dir_sql}" }.join(', ').freeze
|
33
|
+
else
|
34
|
+
"#{col_sql} #{dir_sql}".freeze
|
35
|
+
end
|
34
36
|
end
|
35
37
|
|
36
38
|
SORT_DIRECTIONS = [:asc, :desc].freeze
|
@@ -10,87 +10,97 @@ module OrderQuery
|
|
10
10
|
@point = point
|
11
11
|
end
|
12
12
|
|
13
|
-
#
|
14
|
-
# @
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
13
|
+
# Join condition pairs with OR, and nest within each other with AND
|
14
|
+
# @param [:before or :after] side
|
15
|
+
# @return [query, parameters] WHERE conditions matching records strictly before / after this one
|
16
|
+
# sales < 5 OR
|
17
|
+
# sales = 5 AND (
|
18
|
+
# invoice < 3 OR
|
19
|
+
# invoices = 3 AND (
|
20
|
+
# ... ))
|
21
|
+
def build(side)
|
22
|
+
# generate pairs of terms such as sales < 5, sales = 5
|
23
|
+
parts = point.space.conditions.map { |cond|
|
24
|
+
[where_side(cond, side, true), where_tie(cond)].reject { |x| x == WHERE_IDENTITY }
|
22
25
|
}
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
+
# group pairwise with OR, and nest with AND
|
27
|
+
query = foldr_terms parts.map { |terms| join_terms 'OR'.freeze, *terms }, 'AND'.freeze
|
28
|
+
if ::OrderQuery.wrap_top_level_or
|
29
|
+
# wrap in a redundant AND clause for performance
|
30
|
+
query = wrap_top_level_or query, parts, side
|
31
|
+
end
|
32
|
+
query
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
# @param [String] sql_operator SQL operator
|
38
|
+
# @return [query, params] terms right-folded with sql_operator
|
39
|
+
# [A, B, C, ...] -> A AND (B AND (C AND ...))
|
40
|
+
def foldr_terms(terms, sql_operator)
|
41
|
+
foldr_i WHERE_IDENTITY, terms do |a, b, i|
|
42
|
+
join_terms sql_operator, a, (i > 1 ? wrap_term_with_parens(b) : b)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# joins terms with an operator
|
47
|
+
# @return [query, parameters]
|
48
|
+
def join_terms(op, *terms)
|
49
|
+
[terms.map(&:first).reject(&:empty?).join(" #{op} "), terms.map(&:second).reduce([], :+)]
|
50
|
+
end
|
51
|
+
|
52
|
+
def wrap_term_with_parens(t)
|
53
|
+
["(#{t[0]})", t[1]]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Wrap top level OR clause to help DB with using the index
|
57
|
+
# Before:
|
58
|
+
# (sales < 5 OR
|
59
|
+
# (sales = 5 AND ...))
|
60
|
+
# After:
|
61
|
+
# (sales <= 5 AND
|
62
|
+
# (sales < 5 OR
|
63
|
+
# (sales = 5 AND ...)))
|
64
|
+
# Read more at https://github.com/glebm/order_query/issues/3
|
65
|
+
def wrap_top_level_or(query, pairs, side)
|
26
66
|
top_pair_idx = pairs.index(&:present?)
|
27
67
|
if top_pair_idx &&
|
28
68
|
(top_pair = pairs[top_pair_idx]).length == 2 &&
|
29
|
-
(
|
30
|
-
|
31
|
-
join_terms 'AND'.freeze, redundant_cond, wrap_parens(query)
|
69
|
+
top_pair.first != (redundant_cond = where_side(point.space.conditions[top_pair_idx], side, false))
|
70
|
+
join_terms 'AND'.freeze, redundant_cond, wrap_term_with_parens(query)
|
32
71
|
else
|
33
72
|
query
|
34
73
|
end
|
35
74
|
end
|
36
75
|
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
# x0 OR
|
42
|
-
# y0 AND (x1 OR
|
43
|
-
# y1 AND (x2 OR
|
44
|
-
# y2 AND x3))
|
45
|
-
#
|
46
|
-
# Since x matches order criteria with values that come before / after the current record,
|
47
|
-
# and y matches order criteria with values equal to the current record's value (for resolving ties),
|
48
|
-
# the resulting condition matches just the elements that come before / after the record
|
49
|
-
def group_operators(term_pairs)
|
50
|
-
# create "x OR y" string
|
51
|
-
disjunctive = join_terms 'OR'.freeze, *term_pairs[0]
|
52
|
-
rest = term_pairs.from(1)
|
53
|
-
if rest.present?
|
54
|
-
# nest the remaining pairs recursively, appending them with " AND "
|
55
|
-
rest_grouped = group_operators rest
|
56
|
-
join_terms 'AND'.freeze, disjunctive, (rest.length == 1 ? rest_grouped : wrap_parens(rest_grouped))
|
76
|
+
# @return [query, params] tie-breaker unless condition is unique
|
77
|
+
def where_tie(cond)
|
78
|
+
if cond.unique?
|
79
|
+
WHERE_IDENTITY
|
57
80
|
else
|
58
|
-
|
81
|
+
where_eq(cond)
|
59
82
|
end
|
60
83
|
end
|
61
84
|
|
62
|
-
|
63
|
-
["(#{t[0]})", t[1]]
|
64
|
-
end
|
65
|
-
|
66
|
-
# joins terms with an operator
|
67
|
-
# @return [query, parameters]
|
68
|
-
def join_terms(op, *terms)
|
69
|
-
[terms.map { |t| t.first.presence }.compact.join(" #{op} "),
|
70
|
-
terms.map(&:second).reduce(:+) || []]
|
71
|
-
end
|
72
|
-
|
73
|
-
# @param [:before or :after] mode
|
85
|
+
# @param [:before or :after] side
|
74
86
|
# @return [query, params] return query conditions for attribute values before / after the current one
|
75
|
-
def
|
76
|
-
value = attr_value cond
|
87
|
+
def where_side(cond, side, strict = true, value = point.value(cond))
|
77
88
|
if cond.order_enum
|
78
|
-
values = cond.
|
89
|
+
values = cond.enum_side(value, side, strict)
|
79
90
|
if cond.complete? && values.length == cond.order_enum.length
|
80
91
|
WHERE_IDENTITY
|
81
92
|
else
|
82
93
|
where_in cond, values
|
83
94
|
end
|
84
95
|
else
|
85
|
-
where_ray cond, value,
|
96
|
+
where_ray cond, value, side, strict
|
86
97
|
end
|
87
98
|
end
|
88
99
|
|
89
|
-
|
90
100
|
def where_in(cond, values)
|
91
101
|
case values.length
|
92
102
|
when 0
|
93
|
-
|
103
|
+
WHERE_IDENTITY
|
94
104
|
when 1
|
95
105
|
where_eq cond, values[0]
|
96
106
|
else
|
@@ -98,7 +108,7 @@ module OrderQuery
|
|
98
108
|
end
|
99
109
|
end
|
100
110
|
|
101
|
-
def where_eq(cond, value =
|
111
|
+
def where_eq(cond, value = point.value(cond))
|
102
112
|
[%Q(#{cond.sql.column_name} = ?).freeze, [value]]
|
103
113
|
end
|
104
114
|
|
@@ -110,10 +120,16 @@ module OrderQuery
|
|
110
120
|
end
|
111
121
|
|
112
122
|
WHERE_IDENTITY = [''.freeze, [].freeze].freeze
|
113
|
-
WHERE_NONE = ['∅'.freeze, [].freeze].freeze
|
114
123
|
|
115
|
-
|
116
|
-
|
124
|
+
private
|
125
|
+
|
126
|
+
# Inject with index from right to left, turning [a, b, c] into a + (b + c)
|
127
|
+
# Passes an index to the block, counting from the right
|
128
|
+
# Read more about folds:
|
129
|
+
# * http://www.haskell.org/haskellwiki/Fold
|
130
|
+
# * http://en.wikipedia.org/wiki/Fold_(higher-order_function)
|
131
|
+
def foldr_i(z, xs, &f)
|
132
|
+
xs.reverse_each.each_with_index.inject(z) { |b, (a, i)| f.call a, b, i }
|
117
133
|
end
|
118
134
|
end
|
119
135
|
end
|
data/lib/order_query/version.rb
CHANGED
data/spec/order_query_spec.rb
CHANGED
@@ -88,7 +88,7 @@ describe 'OrderQuery' do
|
|
88
88
|
issues = ds.map do |attr|
|
89
89
|
Issue.new(priority: attr[0], votes: attr[1], suspicious_votes: attr[2], updated_at: attr[3])
|
90
90
|
end
|
91
|
-
issues.reverse_each(&:save!)
|
91
|
+
issues.shuffle.reverse_each(&:save!)
|
92
92
|
expect(Issue.display_order.to_a).to eq(issues)
|
93
93
|
issues.each_slice(2) do |prev, cur|
|
94
94
|
cur ||= issues.first
|