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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 14a1cc9219fd6bb2221967eef338dcabee8854f1
4
- data.tar.gz: e4a8c908c803e2c8afad23a0b278473043087cc7
3
+ metadata.gz: 04dd1e3cb69ac7b1cd363629e1449f2eec4bd921
4
+ data.tar.gz: 1d2533afcfea18fe8a1b06ac63dec068ef59cf8f
5
5
  SHA512:
6
- metadata.gz: e93eb1ec39fab70c1895d34aeef9bbfab92c8da4b7c2962c7a822f4e0df5501331c4a5c733a471fdcfb61b287d4e9cebfd8ea668b258d957755e942011384ee1
7
- data.tar.gz: 4ec32294d12907c1245607f2f2f79a9f205e9f7c7d7396f4b7a1bd1877fb597262cf29e7aa53d81aad512efc36a9737dbf68601f9828f69b7b16adaa77dc77eb
6
+ metadata.gz: f708515aa23195a7cb3051fca8848d1382190429ed9f2055cf0d0bc07b7b66b83c8792d5f1c3fdbbe6c7d3df2f62ec0571cc95755baeec621470c6464c838a05
7
+ data.tar.gz: 7650728e46013f47e129d9a4e939ef531c687bda7315f8326737d628f1b8bee509395a072c079e342d04e2e34640ea9cde5b4f504eed25625ee05082c61f0a3a
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.2.1
2
+
3
+ * `complete` now defaults to true for list attributes as well.
4
+
1
5
  ## 0.2.0
2
6
 
3
7
  * Dynamic query methods renamed to `order_by`
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.0'
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], complete: true],
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, 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 |
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, complete: true],
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 `x0 OR ..` clause is actually wrapped with `x0' AND (x0 OR ...)`, where *x0'* is a non-strict condition,
168
- for [performance reasons](https://github.com/glebm/order_query/issues/3). This can be disabled with `OrderQuery::WhereBuilder.wrap_top_level_or = false`.
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
- @unique = if options.key?(:unique)
21
- !!options[:unique]
22
- else
23
- name.to_s == scope.primary_key
24
- end
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] mode
44
- # @return [Array] valid order values before / after passed (depending on the mode)
45
- def filter_values(value, mode, strict = true)
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 mode == :after && dir == :desc || mode == :before && dir == :asc
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)
@@ -48,19 +48,23 @@ module OrderQuery
48
48
 
49
49
  # @return [ActiveRecord::Relation]
50
50
  def after
51
- records :after
51
+ side :after
52
52
  end
53
53
 
54
54
  # @return [ActiveRecord::Relation]
55
55
  def before
56
- records :before
56
+ side :before
57
57
  end
58
58
 
59
- # @param [:before, :after] direction
59
+ # @param [:before, :after] side
60
60
  # @return [ActiveRecord::Relation]
61
- def records(direction)
62
- query, query_args = @where_sql.build(direction)
63
- scope = (direction == :after ? space.scope : space.reverse_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
- if cond.order_enum
27
- cond.order_enum.map { |v|
28
- "#{cond.sql.column_name}=#{cond.sql.quote v} #{cond.order.to_s.upcase}"
29
- }.join(', ').freeze
30
- else
31
- "#{cond.sql.column_name} #{sort_direction_sql cond.order}".freeze
32
- end
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
- # @param [:before or :after] mode
14
- # @return [query, parameters] conditions that exclude all elements not before / after the current one
15
- def build(mode)
16
- conditions = point.space.conditions
17
- # pairs of [x0, y0]
18
- pairs = conditions.map { |cond|
19
- [where_relative(cond, mode, true), (where_eq(cond) unless cond.unique?)].reject { |x|
20
- x.nil? || x == WHERE_IDENTITY || x == WHERE_NONE
21
- }.compact
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
- query = group_operators pairs
24
- return query unless ::OrderQuery.wrap_top_level_or
25
- # Wrap top level OR clause for performance, see https://github.com/glebm/order_query/issues/3
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
- (top_level_cond = conditions[top_pair_idx]) &&
30
- (redundant_cond = where_relative(top_level_cond, mode, false)) != top_pair.first
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
- # Join condition pairs internally with OR, and nested within each other with AND
38
- # @param [Array] term_pairs of query terms [[x0, y0], [x1, y1], ...],
39
- # xi, yi are pairs of [query, parameters]
40
- # @return [query, parameters]
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
- disjunctive
81
+ where_eq(cond)
59
82
  end
60
83
  end
61
84
 
62
- def wrap_parens(t)
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 where_relative(cond, mode, strict = true)
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.filter_values(value, mode, strict)
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, mode, strict
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
- WHERE_NONE
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 = attr_value(cond))
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
- def attr_value(cond)
116
- point.record.send cond.name
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
@@ -1,3 +1,3 @@
1
1
  module OrderQuery
2
- VERSION = '0.2.0'
2
+ VERSION = '0.2.1'
3
3
  end
@@ -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
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.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Mazovetskiy