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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 65a8e133a25873debf2d301e567ebd385e173aed
4
- data.tar.gz: ba20134c27cc4b03deb1f76c382c9a6a0abd9b0a
3
+ metadata.gz: b77ab79b198ad089f382ace5d234c433f239bff0
4
+ data.tar.gz: 1c2507543963747b645dd4426328fea48b6909e1
5
5
  SHA512:
6
- metadata.gz: f642f0b9d674d7cdd2b1bc7cae39831cc059baf574787640ddeb1d5170059982e4ff9a93629b4c2a57b897991af15d83940a29ce80b14d09b971b88ac51a4719
7
- data.tar.gz: de5fccc4d3e103bd32f2237d5a89d2a70d9df5799e4f205100adf8026200af588214704a6bc92a0ff979017405fac95914c6b758fb6df23ecd61a3a69ad85714
6
+ metadata.gz: 258a2dd941e882fb115a1a15f1f2be6cabe359840d9fe06fbbb6fbbe41dd37284b4cc27909507624367412a65383d6480462cb37d19181acff1602a8c67894b0
7
+ data.tar.gz: d72f77b66611f6ca0f784ac080e1ef6cd99369fe4cf8c279c8a7bdaa3a130a13821df4efceb035ff201772d7f3cde0563b1dac4752b95d6feaee69bb0fd9e624
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.1.3
2
+
3
+ * New condition option `complete` for list conditions for optimized query generation
4
+
1
5
  ## 0.1.2
2
6
 
3
7
  * Wrap top-level `OR` with a redundant `AND` for [performance reasons](https://github.com/glebm/order_query/issues/3).
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.2'
19
+ gem 'order_query', '~> 0.1.3'
20
20
  ```
21
21
 
22
22
  ## Usage
23
23
 
24
- Define the criteria with `order_query`:
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 :order_list, [
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
- Defining the criteria adds `ORDER BY` scopes:
48
+ Order scopes are defined by `order_query`:
40
49
 
41
50
  ```ruby
42
- Post.order_list #=> ActiveRecord::Relation<...>
43
- Post.reverse_order_list #=> ActiveRecord::Relation<...>
51
+ Post.order_for_index #=> ActiveRecord::Relation<...>
52
+ Post.reverse_order_for_index #=> ActiveRecord::Relation<...>
44
53
  ```
45
54
 
46
- ### Records relative to a given one
55
+ ### Before, after, previous, and next
47
56
 
48
- `order_query` also adds an instance method for querying relative to the record:
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).order_list(scope) #=> OrderQuery::RelativeOrder<...>
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
- ### Advanced options
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 = spec.dup
7
- @options = spec.extract_options!
8
- @name = spec[0]
9
- @order = spec[1] || :asc
10
- @order_order = spec[2] || :desc
11
- @scope = scope
12
- @unique = @options.key?(:unique) ? !!@options[:unique] : (name.to_s == scope.primary_key)
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 ray?
20
- !order.is_a?(Array)
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 values_around(value, mode, strict = true)
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
- sql = options[:sql]
44
- if sql
45
- sql = sql.call if sql.respond_to?(:call)
46
- sql
47
- else
48
- scope.connection.quote_table_name(scope.table_name) + '.' + scope.connection.quote_column_name(name)
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
@@ -1,3 +1,3 @@
1
1
  module OrderQuery
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.3'
3
3
  end
@@ -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
- conditions = order.conditions
20
- terms = conditions.map { |cond| [where_mode(cond, mode, true), where_eq(cond)] }
21
- query = group_operators terms
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
- if self.class.wrap_top_level_or && !terms[0].include?(EMPTY_FILTER)
24
- join_terms 'AND'.freeze,
25
- where_mode(conditions.first, mode, false),
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 conditions with operators and parenthesis
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 = term_pairs.from(1)
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 = group_operators rest
51
- rest_grouped[0] = "(#{rest_grouped[0]})" unless rest.length == 1
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
- EMPTY_FILTER = [''.freeze, []]
66
-
67
- # @return [query, params] Unless order attribute is unique, such as id, return ['WHERE value = ?', current value].
68
- def where_eq(cond)
69
- if cond.unique?
70
- EMPTY_FILTER
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
- [%Q(#{cond.col_name_sql} = ?).freeze, [attr_value(cond)]]
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
- EMPTY_FILTER
93
+ WHERE_NONE
87
94
  when 1
88
- ["#{cond.col_name_sql} = ?".freeze, [values]]
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
- # @param [:before or :after] mode
95
- # @return [query, params] return query conditions for attribute values before / after the current one
96
- def where_mode(cond, mode, strict = true)
97
- value = attr_value cond
98
- if cond.ray?
99
- where_ray cond, value, mode, strict
100
- else
101
- # ord is an array of sort values, ordered first to last
102
- # if current not in result set, do not apply filter
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
@@ -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:')
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.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Mazovetskiy