order_query 0.2.0 → 0.2.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 +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
|