order_query 0.1.2 → 0.1.3
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 +22 -15
- data/lib/order_query/order_condition.rb +35 -16
- data/lib/order_query/version.rb +1 -1
- data/lib/order_query/where_builder.rb +46 -38
- data/spec/order_query_spec.rb +2 -2
- data/spec/spec_helper.rb +3 -0
- 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: b77ab79b198ad089f382ace5d234c433f239bff0
|
4
|
+
data.tar.gz: 1c2507543963747b645dd4426328fea48b6909e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 258a2dd941e882fb115a1a15f1f2be6cabe359840d9fe06fbbb6fbbe41dd37284b4cc27909507624367412a65383d6480462cb37d19181acff1602a8c67894b0
|
7
|
+
data.tar.gz: d72f77b66611f6ca0f784ac080e1ef6cd99369fe4cf8c279c8a7bdaa3a130a13821df4efceb035ff201772d7f3cde0563b1dac4752b95d6feaee69bb0fd9e624
|
data/CHANGES.md
CHANGED
data/README.md
CHANGED
@@ -16,40 +16,49 @@ 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.3'
|
20
20
|
```
|
21
21
|
|
22
22
|
## Usage
|
23
23
|
|
24
|
-
Define
|
24
|
+
Define a list of order conditions with `order_query`:
|
25
25
|
|
26
26
|
```ruby
|
27
27
|
class Post < ActiveRecord::Base
|
28
28
|
include OrderQuery
|
29
|
-
order_query :
|
30
|
-
[:pinned, [true, false]],
|
29
|
+
order_query :order_for_index, [
|
30
|
+
[:pinned, [true, false], complete: true],
|
31
31
|
[:published_at, :desc],
|
32
32
|
[:id, :desc]
|
33
33
|
]
|
34
34
|
end
|
35
35
|
```
|
36
36
|
|
37
|
+
An order condition is specified as an attribute name, optionally an ordered list of values, and a sort direction.
|
38
|
+
Additional options are:
|
39
|
+
|
40
|
+
| option | description |
|
41
|
+
|------------|---------------------------------------------------------------------------------------------------------|
|
42
|
+
| unique | Unique attribute, avoids redundant comparisons. Default: `true` for primary key, `false` otherwise. |
|
43
|
+
| complete | Complete attribute, avoids redundant comparisons. Default: `false` for ordered lists, `true` otherwise. |
|
44
|
+
| sql | Customize attribute value SQL |
|
45
|
+
|
37
46
|
### Order scopes
|
38
47
|
|
39
|
-
|
48
|
+
Order scopes are defined by `order_query`:
|
40
49
|
|
41
50
|
```ruby
|
42
|
-
Post.
|
43
|
-
Post.
|
51
|
+
Post.order_for_index #=> ActiveRecord::Relation<...>
|
52
|
+
Post.reverse_order_for_index #=> ActiveRecord::Relation<...>
|
44
53
|
```
|
45
54
|
|
46
|
-
###
|
55
|
+
### Before, after, previous, and next
|
47
56
|
|
48
|
-
|
57
|
+
An method is added by `order_query` to query around a record:
|
49
58
|
|
50
59
|
```ruby
|
51
60
|
# get the order object, scope default: Post.all
|
52
|
-
p = Post.find(31).
|
61
|
+
p = Post.find(31).order_for_index(scope) #=> OrderQuery::RelativeOrder<...>
|
53
62
|
p.before #=> ActiveRecord::Relation<...>
|
54
63
|
p.previous #=> Post<...>
|
55
64
|
# pass true to #next and #previous in order to loop onto the the first / last record
|
@@ -60,9 +69,7 @@ p.next #=> Post<...>
|
|
60
69
|
p.after #=> ActiveRecord::Relation<...>
|
61
70
|
```
|
62
71
|
|
63
|
-
|
64
|
-
|
65
|
-
Pass arrays and custom sql as order conditions:
|
72
|
+
#### Order conditions, advanced example
|
66
73
|
|
67
74
|
```ruby
|
68
75
|
class Issue < ActiveRecord::Base
|
@@ -70,7 +77,7 @@ class Issue < ActiveRecord::Base
|
|
70
77
|
order_query :order_display, [
|
71
78
|
# Pass an array for attribute order, and an optional sort direction for the array,
|
72
79
|
# default is *:desc*, so that first in the array <=> first in the result
|
73
|
-
[:priority, %w(high medium low), :desc],
|
80
|
+
[:priority, %w(high medium low), :desc, complete: true],
|
74
81
|
# Sort attribute can be a method name, provided you pass :sql for the attribute
|
75
82
|
[:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
|
76
83
|
# Default sort order for non-array attributes is :asc, just like SQL
|
@@ -158,7 +165,7 @@ LIMIT 1
|
|
158
165
|
```
|
159
166
|
|
160
167
|
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).
|
168
|
+
for [performance reasons](https://github.com/glebm/order_query/issues/3). This can be disabled with `OrderQuery::WhereBuilder.wrap_top_level_or = false`.
|
162
169
|
|
163
170
|
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
171
|
|
@@ -2,28 +2,45 @@ module OrderQuery
|
|
2
2
|
class OrderCondition
|
3
3
|
attr_reader :name, :order, :order_order, :options, :scope
|
4
4
|
|
5
|
+
# @option spec [String] :unique Mark the attribute as unique to avoid redundant conditions
|
6
|
+
# @option spec [String] :complete Mark the condition's domain as complete to avoid redundant conditions (only for array conditions)
|
5
7
|
def initialize(scope, spec)
|
6
|
-
spec
|
7
|
-
|
8
|
-
@name
|
9
|
-
@order
|
10
|
-
@order_order
|
11
|
-
@
|
12
|
-
@
|
8
|
+
spec = spec.dup
|
9
|
+
options = spec.extract_options!
|
10
|
+
@name = spec[0]
|
11
|
+
@order = spec[1] || :asc
|
12
|
+
@order_order = spec[2] || :desc
|
13
|
+
@options = options
|
14
|
+
@scope = scope
|
15
|
+
@unique = if options.key?(:unique)
|
16
|
+
!!options[:unique]
|
17
|
+
else
|
18
|
+
name.to_s == scope.primary_key
|
19
|
+
end
|
20
|
+
@complete = if options.key?(:complete)
|
21
|
+
!!options[:complete]
|
22
|
+
else
|
23
|
+
!list?
|
24
|
+
end
|
13
25
|
end
|
14
26
|
|
15
27
|
def unique?
|
16
28
|
@unique
|
17
29
|
end
|
18
30
|
|
19
|
-
def
|
20
|
-
|
31
|
+
def complete?
|
32
|
+
@complete
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Boolean] whether order is specified as a list of values
|
36
|
+
def list?
|
37
|
+
order.is_a?(Enumerable)
|
21
38
|
end
|
22
39
|
|
23
40
|
# @param [Object] value
|
24
41
|
# @param [:before, :after] mode
|
25
42
|
# @return [Array] valid order values before / after passed (depending on the mode)
|
26
|
-
def
|
43
|
+
def filter_values(value, mode, strict = true)
|
27
44
|
ord = order
|
28
45
|
pos = ord.index(value)
|
29
46
|
if pos
|
@@ -40,12 +57,14 @@ module OrderQuery
|
|
40
57
|
end
|
41
58
|
|
42
59
|
def col_name_sql
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
60
|
+
@col_name_sql ||= begin
|
61
|
+
sql = options[:sql]
|
62
|
+
if sql
|
63
|
+
sql = sql.call if sql.respond_to?(:call)
|
64
|
+
sql
|
65
|
+
else
|
66
|
+
scope.connection.quote_table_name(scope.table_name) + '.' + scope.connection.quote_column_name(name)
|
67
|
+
end
|
49
68
|
end
|
50
69
|
end
|
51
70
|
end
|
data/lib/order_query/version.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
module OrderQuery
|
2
3
|
# Build where clause for searching around a record in an order space
|
3
4
|
class WhereBuilder
|
@@ -16,20 +17,24 @@ module OrderQuery
|
|
16
17
|
# @param [:before or :after] mode
|
17
18
|
# @return [query, parameters] conditions that exclude all elements not before / after the current one
|
18
19
|
def build_query(mode)
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
# pairs of [x0, y0]
|
21
|
+
pairs = order.conditions.map { |cond|
|
22
|
+
[where_relative(cond, mode, true), (where_eq(cond) unless cond.unique?)].reject { |x|
|
23
|
+
x.nil? || x == WHERE_IDENTITY || x == WHERE_NONE
|
24
|
+
}.compact
|
25
|
+
}
|
26
|
+
query = group_operators pairs
|
27
|
+
return query unless self.class.wrap_top_level_or
|
22
28
|
# Wrap top level OR clause for performance, see https://github.com/glebm/order_query/issues/3
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
["(#{query[0]})", query[1]]
|
29
|
+
top_pair_idx = pairs.index(&:present?)
|
30
|
+
if top_pair_idx && pairs[top_pair_idx].length == 2 && (top_level_cond = order.conditions[top_pair_idx])
|
31
|
+
join_terms 'AND'.freeze, where_relative(top_level_cond, mode, false), wrap_parens(query)
|
27
32
|
else
|
28
33
|
query
|
29
34
|
end
|
30
35
|
end
|
31
36
|
|
32
|
-
# Join
|
37
|
+
# Join condition pairs internally with OR, and nested within each other with AND
|
33
38
|
# @param [Array] term_pairs of query terms [[x0, y0], [x1, y1], ...],
|
34
39
|
# xi, yi are pairs of [query, parameters]
|
35
40
|
# @return [query, parameters]
|
@@ -44,17 +49,20 @@ module OrderQuery
|
|
44
49
|
def group_operators(term_pairs)
|
45
50
|
# create "x OR y" string
|
46
51
|
disjunctive = join_terms 'OR'.freeze, *term_pairs[0]
|
47
|
-
rest
|
52
|
+
rest = term_pairs.from(1)
|
48
53
|
if rest.present?
|
49
54
|
# nest the remaining pairs recursively, appending them with " AND "
|
50
|
-
rest_grouped
|
51
|
-
|
52
|
-
join_terms 'AND'.freeze, disjunctive, rest_grouped
|
55
|
+
rest_grouped = group_operators rest
|
56
|
+
join_terms 'AND'.freeze, disjunctive, (rest.length == 1 ? rest_grouped : wrap_parens(rest_grouped))
|
53
57
|
else
|
54
58
|
disjunctive
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
62
|
+
def wrap_parens(t)
|
63
|
+
["(#{t[0]})", t[1]]
|
64
|
+
end
|
65
|
+
|
58
66
|
# joins terms with an operator
|
59
67
|
# @return [query, parameters]
|
60
68
|
def join_terms(op, *terms)
|
@@ -62,48 +70,48 @@ module OrderQuery
|
|
62
70
|
terms.map(&:second).reduce(:+) || []]
|
63
71
|
end
|
64
72
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
if cond.
|
70
|
-
|
73
|
+
# @param [:before or :after] mode
|
74
|
+
# @return [query, params] return query conditions for attribute values before / after the current one
|
75
|
+
def where_relative(cond, mode, strict = true, skip_complete = true)
|
76
|
+
value = attr_value cond
|
77
|
+
if cond.list?
|
78
|
+
values = cond.filter_values(value, mode, strict)
|
79
|
+
if cond.complete? && values.length == cond.order.length
|
80
|
+
WHERE_IDENTITY
|
81
|
+
else
|
82
|
+
where_in cond, values
|
83
|
+
end
|
71
84
|
else
|
72
|
-
|
85
|
+
where_ray cond, value, mode, strict
|
73
86
|
end
|
74
87
|
end
|
75
88
|
|
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
89
|
|
83
90
|
def where_in(cond, values)
|
84
91
|
case values.length
|
85
92
|
when 0
|
86
|
-
|
93
|
+
WHERE_NONE
|
87
94
|
when 1
|
88
|
-
|
95
|
+
where_eq cond, values[0]
|
89
96
|
else
|
90
97
|
["#{cond.col_name_sql} IN (?)".freeze, [values]]
|
91
98
|
end
|
92
99
|
end
|
93
100
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
where_in cond, cond.values_around(value, mode, strict)
|
104
|
-
end
|
101
|
+
def where_eq(cond, value = attr_value(cond))
|
102
|
+
[%Q(#{cond.col_name_sql} = ?).freeze, [value]]
|
103
|
+
end
|
104
|
+
|
105
|
+
def where_ray(cond, from, mode, strict = true)
|
106
|
+
ops = %w(< >)
|
107
|
+
ops = ops.reverse if mode == :after
|
108
|
+
op = {asc: ops[0], desc: ops[1]}[cond.order || :asc]
|
109
|
+
["#{cond.col_name_sql} #{op}#{'=' unless strict} ?".freeze, [from]]
|
105
110
|
end
|
106
111
|
|
112
|
+
WHERE_IDENTITY = [''.freeze, [].freeze].freeze
|
113
|
+
WHERE_NONE = ['∅'.freeze, [].freeze].freeze
|
114
|
+
|
107
115
|
def attr_value(cond)
|
108
116
|
record.send cond.name
|
109
117
|
end
|
data/spec/order_query_spec.rb
CHANGED
@@ -4,7 +4,7 @@ require 'spec_helper'
|
|
4
4
|
class Post < ActiveRecord::Base
|
5
5
|
include OrderQuery
|
6
6
|
order_query :order_list, [
|
7
|
-
[:pinned, [true, false]],
|
7
|
+
[:pinned, [true, false], complete: true],
|
8
8
|
[:published_at, :desc],
|
9
9
|
[:id, :desc]
|
10
10
|
]
|
@@ -17,7 +17,7 @@ end
|
|
17
17
|
# Advanced example
|
18
18
|
class Issue < ActiveRecord::Base
|
19
19
|
DISPLAY_ORDER = [
|
20
|
-
[:priority, %w(high medium low)],
|
20
|
+
[:priority, %w(high medium low), complete: true],
|
21
21
|
[:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
|
22
22
|
[:updated_at, :desc],
|
23
23
|
[:id, :desc]
|
data/spec/spec_helper.rb
CHANGED
@@ -13,4 +13,7 @@ require 'order_query'
|
|
13
13
|
|
14
14
|
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
15
15
|
|
16
|
+
require 'fileutils'
|
17
|
+
FileUtils.mkpath 'log' unless File.directory? 'log'
|
18
|
+
ActiveRecord::Base.logger = Logger.new('log/test-queries.log')
|
16
19
|
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|