order_query 0.1.2 → 0.1.3
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 +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:')
|