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