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 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