order_query 0.3.0 → 0.3.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 +7 -1
- data/MIT-LICENSE +1 -1
- data/README.md +7 -8
- data/lib/order_query/{condition.rb → column.rb} +6 -13
- data/lib/order_query/space.rb +13 -8
- data/lib/order_query/sql/{condition.rb → column.rb} +6 -6
- data/lib/order_query/sql/order_by.rb +5 -5
- data/lib/order_query/sql/where.rb +31 -37
- data/lib/order_query/version.rb +1 -1
- data/lib/order_query.rb +3 -3
- data/spec/order_query_spec.rb +9 -5
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be66ba349bb0a4819d39ee8f01d8bfa12e82177e
|
4
|
+
data.tar.gz: 56637889ea73d1f90e70dcd72e09431eb357f1e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8f9fe60869ae746f9d095dfc549f651b994ded8b75875cd60761e9e7683cdcb6968834916ae6eb881b3dc684f26220adf4e873948923b8e41daee90b11d6671
|
7
|
+
data.tar.gz: b068a5d7bcc50af28dbdff396adca897f8f502640fcad0f162f1ea9ee4424c9a9cf1afde85837261908bd984a9339bbb6fdce50477d0aa9ed99b394cabf2e53a
|
data/CHANGES.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
+
## 0.3.1
|
2
|
+
|
3
|
+
* Automatically add primary key when there is no unique column for the order
|
4
|
+
* Remove `complete` option
|
5
|
+
* Fix Rubinius compatibility
|
6
|
+
|
1
7
|
## 0.3.0
|
2
8
|
|
3
|
-
* `order_query` now accepts
|
9
|
+
* `order_query` now accepts columns as varargs. Array form is still supported.
|
4
10
|
* `order_by` renamed to `seek`
|
5
11
|
|
6
12
|
## 0.2.1
|
data/MIT-LICENSE
CHANGED
@@ -6,7 +6,7 @@ a copy of this software and associated documentation files (the
|
|
6
6
|
without limitation the rights to use, copy, modify, merge, publish,
|
7
7
|
distribute, sublicense, and/or sell copies of the Software, and to
|
8
8
|
permit persons to whom the Software is furnished to do so, subject to
|
9
|
-
the following
|
9
|
+
the following columns:
|
10
10
|
|
11
11
|
The above copyright notice and this permission notice shall be
|
12
12
|
included in all copies or substantial portions of the Software.
|
data/README.md
CHANGED
@@ -12,12 +12,12 @@ It uses [keyset pagination](http://use-the-index-luke.com/no-offset) to achieve
|
|
12
12
|
Add to Gemfile:
|
13
13
|
|
14
14
|
```ruby
|
15
|
-
gem 'order_query', '~> 0.3.
|
15
|
+
gem 'order_query', '~> 0.3.1'
|
16
16
|
```
|
17
17
|
|
18
18
|
## Usage
|
19
19
|
|
20
|
-
Define named order
|
20
|
+
Define named order columns with `order_query`:
|
21
21
|
|
22
22
|
```ruby
|
23
23
|
class Post < ActiveRecord::Base
|
@@ -29,7 +29,7 @@ class Post < ActiveRecord::Base
|
|
29
29
|
end
|
30
30
|
```
|
31
31
|
|
32
|
-
Order query accepts a list of order
|
32
|
+
Order query accepts a list of order columns as varargs or one array, each one specified as:
|
33
33
|
|
34
34
|
```ruby
|
35
35
|
[<attribute name>, (attribute values in order), (:asc or :desc), (options hash)]
|
@@ -40,7 +40,6 @@ Available options:
|
|
40
40
|
| option | description |
|
41
41
|
|------------|----------------------------------------------------------------------------|
|
42
42
|
| unique | Unique attribute. Default: `true` for primary key, `false` otherwise. |
|
43
|
-
| complete | Specified attribute values are the only possible values. Default: `true`. |
|
44
43
|
| sql | Customize attribute value SQL |
|
45
44
|
|
46
45
|
|
@@ -87,9 +86,9 @@ post = posts.find(42)
|
|
87
86
|
post.order_home(posts) #=> #<OrderQuery::Point>
|
88
87
|
```
|
89
88
|
|
90
|
-
### Dynamic
|
89
|
+
### Dynamic columns
|
91
90
|
|
92
|
-
Query with dynamic order
|
91
|
+
Query with dynamic order columns using the `seek(*spec)` class method:
|
93
92
|
|
94
93
|
```ruby
|
95
94
|
space = Post.visible.seek([:id, :desc]) #=> #<OrderQuery::Space>
|
@@ -124,7 +123,7 @@ class Post < ActiveRecord::Base
|
|
124
123
|
[:priority, %w(high medium low)],
|
125
124
|
# A method and custom SQL can be used instead of an attribute
|
126
125
|
[:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
|
127
|
-
# Default sort order for non-array
|
126
|
+
# Default sort order for non-array columns is :asc, just like SQL
|
128
127
|
[:updated_at, :desc],
|
129
128
|
# pass unique: true for unique attributes to get more optimized queries
|
130
129
|
# unique is true by default for primary_key
|
@@ -153,7 +152,7 @@ ORDER BY
|
|
153
152
|
LIMIT 1
|
154
153
|
```
|
155
154
|
|
156
|
-
The actual query is a bit different because `order_query` wraps the top-level `OR` with a (redundant) non-strict
|
155
|
+
The actual query is a bit different because `order_query` wraps the top-level `OR` with a (redundant) non-strict column `x0' AND (x0 OR ...)`
|
157
156
|
for [performance reasons](https://github.com/glebm/order_query/issues/3).
|
158
157
|
This can be disabled with `OrderQuery.wrap_top_level_or = false`.
|
159
158
|
|
@@ -1,13 +1,12 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
require 'order_query/sql/
|
2
|
+
require 'order_query/sql/column'
|
3
3
|
module OrderQuery
|
4
|
-
# An order
|
5
|
-
class
|
4
|
+
# An order column (sort column)
|
5
|
+
class Column
|
6
6
|
attr_reader :name, :order, :order_enum, :options
|
7
7
|
delegate :column_name, :quote, to: :@sql
|
8
8
|
|
9
|
-
# @option spec [String] :unique Mark the attribute as unique to avoid redundant
|
10
|
-
# @option spec [String] :complete Mark the condition's domain as complete to avoid redundant conditions (only for array conditions)
|
9
|
+
# @option spec [String] :unique Mark the attribute as unique to avoid redundant columns
|
11
10
|
def initialize(spec, scope)
|
12
11
|
spec = spec.dup
|
13
12
|
options = spec.extract_options!
|
@@ -24,18 +23,13 @@ module OrderQuery
|
|
24
23
|
complete: true
|
25
24
|
)
|
26
25
|
@unique = @options[:unique]
|
27
|
-
@
|
28
|
-
@sql = SQL::Condition.new(self, scope)
|
26
|
+
@sql = SQL::Column.new(self, scope)
|
29
27
|
end
|
30
28
|
|
31
29
|
def unique?
|
32
30
|
@unique
|
33
31
|
end
|
34
32
|
|
35
|
-
def complete?
|
36
|
-
@complete
|
37
|
-
end
|
38
|
-
|
39
33
|
# @param [Object] value
|
40
34
|
# @param [:before, :after] side
|
41
35
|
# @return [Array] valid order values before / after passed (depending on the side)
|
@@ -54,7 +48,7 @@ module OrderQuery
|
|
54
48
|
end
|
55
49
|
else
|
56
50
|
# default to all if current is not in sort order values
|
57
|
-
|
51
|
+
[]
|
58
52
|
end
|
59
53
|
end
|
60
54
|
|
@@ -63,7 +57,6 @@ module OrderQuery
|
|
63
57
|
@name,
|
64
58
|
(@order_enum.inspect if order_enum),
|
65
59
|
('unique' if @unique),
|
66
|
-
('complete' if order_enum && complete?),
|
67
60
|
(column_name if options[:sql]),
|
68
61
|
{desc: '▼', asc: '▲'}[@order]
|
69
62
|
].compact
|
data/lib/order_query/space.rb
CHANGED
@@ -1,17 +1,22 @@
|
|
1
|
-
require 'order_query/
|
1
|
+
require 'order_query/column'
|
2
2
|
require 'order_query/sql/order_by'
|
3
3
|
module OrderQuery
|
4
4
|
# Order specification and a scope
|
5
5
|
class Space
|
6
|
-
# @return [Array<OrderQuery::
|
7
|
-
attr_reader :
|
6
|
+
# @return [Array<OrderQuery::Column>]
|
7
|
+
attr_reader :columns
|
8
8
|
|
9
9
|
# @param [ActiveRecord::Relation] base_scope
|
10
10
|
# @param [Array<Array<Symbol,String>>, OrderQuery::Spec] order_spec
|
11
11
|
def initialize(base_scope, order_spec)
|
12
12
|
@base_scope = base_scope
|
13
|
-
@
|
14
|
-
|
13
|
+
@columns = order_spec.map { |cond_spec| Column.new(cond_spec, base_scope) }
|
14
|
+
# add primary key if columns are not unique
|
15
|
+
unless @columns.last.unique?
|
16
|
+
raise ArgumentError.new('Unique column must be last') if @columns.detect(&:unique?)
|
17
|
+
@columns << Column.new([base_scope.primary_key], base_scope)
|
18
|
+
end
|
19
|
+
@order_by_sql = SQL::OrderBy.new(@columns)
|
15
20
|
end
|
16
21
|
|
17
22
|
# @return [Point]
|
@@ -19,12 +24,12 @@ module OrderQuery
|
|
19
24
|
Point.new(record, self)
|
20
25
|
end
|
21
26
|
|
22
|
-
# @return [ActiveRecord::Relation] scope ordered by
|
27
|
+
# @return [ActiveRecord::Relation] scope ordered by columns
|
23
28
|
def scope
|
24
29
|
@scope ||= @base_scope.order(@order_by_sql.build)
|
25
30
|
end
|
26
31
|
|
27
|
-
# @return [ActiveRecord::Relation] scope ordered by
|
32
|
+
# @return [ActiveRecord::Relation] scope ordered by columns in reverse
|
28
33
|
def scope_reverse
|
29
34
|
@scope_reverse ||= @base_scope.order(@order_by_sql.build_reverse)
|
30
35
|
end
|
@@ -42,7 +47,7 @@ module OrderQuery
|
|
42
47
|
delegate :count, :empty?, to: :@base_scope
|
43
48
|
|
44
49
|
def inspect
|
45
|
-
"#<OrderQuery::Space @
|
50
|
+
"#<OrderQuery::Space @columns=#{@columns.inspect} @base_scope=#{@base_scope.inspect}>"
|
46
51
|
end
|
47
52
|
end
|
48
53
|
end
|
@@ -1,20 +1,20 @@
|
|
1
1
|
module OrderQuery
|
2
2
|
module SQL
|
3
|
-
class
|
4
|
-
attr_reader :
|
3
|
+
class Column
|
4
|
+
attr_reader :column, :scope
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@
|
6
|
+
def initialize(column, scope)
|
7
|
+
@column = column
|
8
8
|
@scope = scope
|
9
9
|
end
|
10
10
|
|
11
11
|
def column_name
|
12
12
|
@column_name ||= begin
|
13
|
-
sql =
|
13
|
+
sql = column.options[:sql]
|
14
14
|
if sql
|
15
15
|
sql.respond_to?(:call) ? sql.call : sql
|
16
16
|
else
|
17
|
-
connection.quote_table_name(scope.table_name) + '.' + connection.quote_column_name(
|
17
|
+
connection.quote_table_name(scope.table_name) + '.' + connection.quote_column_name(column.name)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module OrderQuery
|
2
2
|
module SQL
|
3
3
|
class OrderBy
|
4
|
-
# @param [Array<
|
5
|
-
def initialize(
|
6
|
-
@
|
4
|
+
# @param [Array<Column>]
|
5
|
+
def initialize(columns)
|
6
|
+
@columns = columns
|
7
7
|
end
|
8
8
|
|
9
9
|
# @return [String]
|
@@ -20,10 +20,10 @@ module OrderQuery
|
|
20
20
|
|
21
21
|
# @return [Array<String>]
|
22
22
|
def order_by_sql_clauses
|
23
|
-
@
|
23
|
+
@columns.map { |cond| column_clause cond }
|
24
24
|
end
|
25
25
|
|
26
|
-
def
|
26
|
+
def column_clause(cond)
|
27
27
|
dir_sql = sort_direction_sql cond.order
|
28
28
|
col_sql = cond.column_name
|
29
29
|
if cond.order_enum
|
@@ -7,13 +7,13 @@ module OrderQuery
|
|
7
7
|
|
8
8
|
# @param [OrderQuery::Point] point
|
9
9
|
def initialize(point)
|
10
|
-
@point
|
11
|
-
@
|
10
|
+
@point = point
|
11
|
+
@columns = point.space.columns
|
12
12
|
end
|
13
13
|
|
14
|
-
# Join
|
14
|
+
# Join column pairs with OR, and nest within each other with AND
|
15
15
|
# @param [:before or :after] side
|
16
|
-
# @return [query, parameters] WHERE
|
16
|
+
# @return [query, parameters] WHERE columns matching records strictly before / after this one
|
17
17
|
# sales < 5 OR
|
18
18
|
# sales = 5 AND (
|
19
19
|
# invoice < 3 OR
|
@@ -21,14 +21,14 @@ module OrderQuery
|
|
21
21
|
# ... ))
|
22
22
|
def build(side)
|
23
23
|
# generate pairs of terms such as sales < 5, sales = 5
|
24
|
-
|
25
|
-
[where_side(
|
24
|
+
terms = @columns.map { |col|
|
25
|
+
[where_side(col, side, true), where_tie(col)].reject { |x| x == WHERE_IDENTITY }
|
26
26
|
}
|
27
27
|
# group pairwise with OR, and nest with AND
|
28
|
-
query = foldr_terms
|
28
|
+
query = foldr_terms terms.map { |pair| join_terms 'OR'.freeze, *pair }, 'AND'.freeze
|
29
29
|
if ::OrderQuery.wrap_top_level_or
|
30
30
|
# wrap in a redundant AND clause for performance
|
31
|
-
query = wrap_top_level_or query,
|
31
|
+
query = wrap_top_level_or query, terms, side
|
32
32
|
end
|
33
33
|
query
|
34
34
|
end
|
@@ -44,7 +44,7 @@ module OrderQuery
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
# joins terms with an operator
|
47
|
+
# joins terms with an operator, empty terms are skipped
|
48
48
|
# @return [query, parameters]
|
49
49
|
def join_terms(op, *terms)
|
50
50
|
[terms.map(&:first).reject(&:empty?).join(" #{op} "), terms.map(&:second).reduce([], :+)]
|
@@ -63,61 +63,55 @@ module OrderQuery
|
|
63
63
|
# (sales < 5 OR
|
64
64
|
# (sales = 5 AND ...)))
|
65
65
|
# Read more at https://github.com/glebm/order_query/issues/3
|
66
|
-
def wrap_top_level_or(query,
|
67
|
-
|
68
|
-
if
|
69
|
-
(
|
70
|
-
|
71
|
-
join_terms 'AND'.freeze, redundant_cond, wrap_term_with_parens(query)
|
66
|
+
def wrap_top_level_or(query, terms, side)
|
67
|
+
top_term_i = terms.index(&:present?)
|
68
|
+
if top_term_i && terms[top_term_i].length == 2 &&
|
69
|
+
(cond = where_side(@columns[top_term_i], side, false)) != WHERE_IDENTITY
|
70
|
+
join_terms 'AND'.freeze, cond, wrap_term_with_parens(query)
|
72
71
|
else
|
73
72
|
query
|
74
73
|
end
|
75
74
|
end
|
76
75
|
|
77
|
-
# @return [query, params] tie-breaker unless
|
78
|
-
def where_tie(
|
79
|
-
if
|
76
|
+
# @return [query, params] tie-breaker unless column is unique
|
77
|
+
def where_tie(col)
|
78
|
+
if col.unique?
|
80
79
|
WHERE_IDENTITY
|
81
80
|
else
|
82
|
-
where_eq(
|
81
|
+
where_eq(col)
|
83
82
|
end
|
84
83
|
end
|
85
84
|
|
86
85
|
# @param [:before or :after] side
|
87
|
-
# @return [query, params] return query
|
88
|
-
def where_side(
|
89
|
-
if
|
90
|
-
|
91
|
-
if cond.complete? && values.length == cond.order_enum.length
|
92
|
-
WHERE_IDENTITY
|
93
|
-
else
|
94
|
-
where_in cond, values
|
95
|
-
end
|
86
|
+
# @return [query, params] return query fragment for column values before / after the current one
|
87
|
+
def where_side(col, side, strict = true, value = point.value(col))
|
88
|
+
if col.order_enum
|
89
|
+
where_in col, col.enum_side(value, side, strict)
|
96
90
|
else
|
97
|
-
where_ray
|
91
|
+
where_ray col, value, side, strict
|
98
92
|
end
|
99
93
|
end
|
100
94
|
|
101
|
-
def where_in(
|
95
|
+
def where_in(col, values)
|
102
96
|
case values.length
|
103
97
|
when 0
|
104
98
|
WHERE_IDENTITY
|
105
99
|
when 1
|
106
|
-
where_eq
|
100
|
+
where_eq col, values[0]
|
107
101
|
else
|
108
|
-
["#{
|
102
|
+
["#{col.column_name} IN (?)".freeze, [values]]
|
109
103
|
end
|
110
104
|
end
|
111
105
|
|
112
|
-
def where_eq(
|
113
|
-
[%Q(#{
|
106
|
+
def where_eq(col, value = point.value(col))
|
107
|
+
[%Q(#{col.column_name} = ?).freeze, [value]]
|
114
108
|
end
|
115
109
|
|
116
|
-
def where_ray(
|
110
|
+
def where_ray(col, from, mode, strict = true)
|
117
111
|
ops = %w(< >)
|
118
112
|
ops = ops.reverse if mode == :after
|
119
|
-
op = {asc: ops[0], desc: ops[1]}[
|
120
|
-
["#{
|
113
|
+
op = {asc: ops[0], desc: ops[1]}[col.order || :asc]
|
114
|
+
["#{col.column_name} #{op}#{'=' unless strict} ?".freeze, [from]]
|
121
115
|
end
|
122
116
|
|
123
117
|
WHERE_IDENTITY = [''.freeze, [].freeze].freeze
|
data/lib/order_query/version.rb
CHANGED
data/lib/order_query.rb
CHANGED
@@ -66,10 +66,10 @@ module OrderQuery
|
|
66
66
|
scope name, -> {
|
67
67
|
send(space_method).scope
|
68
68
|
}
|
69
|
-
scope
|
69
|
+
scope "#{name}_reverse", -> {
|
70
70
|
send(space_method).scope_reverse
|
71
71
|
}
|
72
|
-
define_singleton_method "#{name}_at", ->
|
72
|
+
define_singleton_method "#{name}_at", ->(record) {
|
73
73
|
send(space_method).at(record)
|
74
74
|
}
|
75
75
|
define_method(name) { |scope = nil|
|
@@ -81,6 +81,6 @@ module OrderQuery
|
|
81
81
|
class << self
|
82
82
|
attr_accessor :wrap_top_level_or
|
83
83
|
end
|
84
|
-
# Wrap top-level or with an AND and a redundant
|
84
|
+
# Wrap top-level or with an AND and a redundant column for performance
|
85
85
|
self.wrap_top_level_or = true
|
86
86
|
end
|
data/spec/order_query_spec.rb
CHANGED
@@ -9,7 +9,7 @@ end
|
|
9
9
|
class Post < ActiveRecord::Base
|
10
10
|
include OrderQuery
|
11
11
|
order_query :order_list,
|
12
|
-
[:pinned, [true, false]
|
12
|
+
[:pinned, [true, false]],
|
13
13
|
[:published_at, :desc],
|
14
14
|
[:id, :desc]
|
15
15
|
end
|
@@ -21,7 +21,8 @@ end
|
|
21
21
|
# Advanced model
|
22
22
|
class Issue < ActiveRecord::Base
|
23
23
|
DISPLAY_ORDER = [
|
24
|
-
[:
|
24
|
+
[:pinned, [true, false]],
|
25
|
+
[:priority, %w(high medium low)],
|
25
26
|
[:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
|
26
27
|
[:updated_at, :desc],
|
27
28
|
[:id, :desc]
|
@@ -63,9 +64,11 @@ describe 'OrderQuery' do
|
|
63
64
|
t = Time.now
|
64
65
|
datasets = [
|
65
66
|
[
|
67
|
+
['high', 5, 0, t, true],
|
68
|
+
['high', 5, 1, t, true],
|
66
69
|
['high', 5, 0, t],
|
70
|
+
['high', 5, 0, t - 1.day],
|
67
71
|
['high', 5, 1, t],
|
68
|
-
['high', 5, 1, t - 1.day],
|
69
72
|
['medium', 10, 0, t],
|
70
73
|
['medium', 10, 5, t - 12.hours],
|
71
74
|
['low', 30, 0, t + 1.day]
|
@@ -85,7 +88,7 @@ describe 'OrderQuery' do
|
|
85
88
|
datasets.each_with_index do |ds, i|
|
86
89
|
it "is ordered correctly (test data #{i})" do
|
87
90
|
issues = ds.map do |attr|
|
88
|
-
Issue.new(priority: attr[0], votes: attr[1], suspicious_votes: attr[2], updated_at: attr[3])
|
91
|
+
Issue.new(priority: attr[0], votes: attr[1], suspicious_votes: attr[2], updated_at: attr[3], pinned: attr[4] || false)
|
89
92
|
end
|
90
93
|
issues.shuffle.reverse_each(&:save!)
|
91
94
|
expect(Issue.display_order.to_a).to eq(issues)
|
@@ -161,7 +164,7 @@ describe 'OrderQuery' do
|
|
161
164
|
end
|
162
165
|
end
|
163
166
|
|
164
|
-
it '#seek falls back to scope when order
|
167
|
+
it '#seek falls back to scope when order column is missing self' do
|
165
168
|
a = create_issue(priority: 'medium')
|
166
169
|
b = create_issue(priority: 'high')
|
167
170
|
expect(a.seek(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
|
@@ -176,6 +179,7 @@ describe 'OrderQuery' do
|
|
176
179
|
self.verbose = false
|
177
180
|
|
178
181
|
create_table :issues do |t|
|
182
|
+
t.column :pinned, :boolean, null: false, default: false
|
179
183
|
t.column :priority, :string
|
180
184
|
t.column :votes, :integer
|
181
185
|
t.column :suspicious_votes, :integer
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: order_query
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gleb Mazovetskiy
|
@@ -78,10 +78,10 @@ files:
|
|
78
78
|
- README.md
|
79
79
|
- Rakefile
|
80
80
|
- lib/order_query.rb
|
81
|
-
- lib/order_query/
|
81
|
+
- lib/order_query/column.rb
|
82
82
|
- lib/order_query/point.rb
|
83
83
|
- lib/order_query/space.rb
|
84
|
-
- lib/order_query/sql/
|
84
|
+
- lib/order_query/sql/column.rb
|
85
85
|
- lib/order_query/sql/order_by.rb
|
86
86
|
- lib/order_query/sql/where.rb
|
87
87
|
- lib/order_query/version.rb
|