order_query 0.1.1 → 0.1.2

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: 2e1364db02e6ba3b3d20bf11d77103dc33a1d287
4
- data.tar.gz: 70f8f66433ef24caebe56b83a165073ac7377f88
3
+ metadata.gz: 65a8e133a25873debf2d301e567ebd385e173aed
4
+ data.tar.gz: ba20134c27cc4b03deb1f76c382c9a6a0abd9b0a
5
5
  SHA512:
6
- metadata.gz: 2ea5b0e8beb0268407ce15504ce0d57200c41fe4f059782c220c7c2b84c40205dd12123eb61f67e9b01d1c287b836123424ae6925d585142bea19b0077d2cf78
7
- data.tar.gz: 3a9dc43bab64321c33f27d2869f895cb36efab27fea662919e61a49d7454863fef8c83ee16a50350b6beb789b51f86936b80636ecac4dfdf1b948a44ce4b0057
6
+ metadata.gz: f642f0b9d674d7cdd2b1bc7cae39831cc059baf574787640ddeb1d5170059982e4ff9a93629b4c2a57b897991af15d83940a29ce80b14d09b971b88ac51a4719
7
+ data.tar.gz: de5fccc4d3e103bd32f2237d5a89d2a70d9df5799e4f205100adf8026200af588214704a6bc92a0ff979017405fac95914c6b758fb6df23ecd61a3a69ad85714
data/CHANGES.md CHANGED
@@ -1,6 +1,11 @@
1
+ ## 0.1.2
2
+
3
+ * Wrap top-level `OR` with a redundant `AND` for [performance reasons](https://github.com/glebm/order_query/issues/3).
4
+ * Remove redundant parens from the query
5
+
1
6
  ## 0.1.1
2
7
 
3
- * `#next(true)` and `#previous(true)` return nil if there is only one record in total.
8
+ * `#next(true)` and `#previous(true)` return `nil` if there is only one record in total.
4
9
 
5
10
  ## 0.1.0
6
11
 
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ gemspec
4
4
 
5
5
  group :test, :development do
6
6
  gem 'coveralls', require: false
7
+ gem 'byebug', platform: :mri_21, require: false
7
8
  end
8
9
 
9
10
  platform :mri, :rbx do
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.1.1'
19
+ gem 'order_query', '~> 0.1.2'
20
20
  ```
21
21
 
22
22
  ## Usage
@@ -43,7 +43,7 @@ Post.order_list #=> ActiveRecord::Relation<...>
43
43
  Post.reverse_order_list #=> ActiveRecord::Relation<...>
44
44
  ```
45
45
 
46
- ### Relative order
46
+ ### Records relative to a given one
47
47
 
48
48
  `order_query` also adds an instance method for querying relative to the record:
49
49
 
@@ -62,7 +62,7 @@ p.after #=> ActiveRecord::Relation<...>
62
62
 
63
63
  ### Advanced options
64
64
 
65
- There is a number of advanced options to help you:
65
+ Pass arrays and custom sql as order conditions:
66
66
 
67
67
  ```ruby
68
68
  class Issue < ActiveRecord::Base
@@ -85,10 +85,9 @@ class Issue < ActiveRecord::Base
85
85
  end
86
86
  ```
87
87
 
88
- ### Dynamic criteria
88
+ ### Dynamic order conditions
89
89
 
90
- Including `OrderQuery` adds `.order_by_query` and `#relative_order_by_query`.
91
- These methods can be called directly directly with the order criteria:
90
+ To query with dynamic order conditions use `Model.order_by_query` and `Model#relative_order_by_query`:
92
91
 
93
92
  ```ruby
94
93
  Issue.order_by_query([[:id, :desc]]) #=> ActiveRecord::Relation<...>
@@ -97,12 +96,11 @@ Issue.find(31).relative_order_by_query([[:id, :desc]]).next #=> Issue<...>
97
96
  Issue.find(31).relative_order_by_query(Issue.visible, [[:id, :desc]]).next #=> Issue<...>
98
97
  ```
99
98
 
100
- This is especially helpful if the order criteria is dynamic, so `order_query` cannot be used to define them beforehand.
101
99
  For example, consider ordering by a list of ids returned from an elasticsearh query:
102
100
 
103
101
  ```ruby
104
102
  ids = Issue.keyword_search('ruby') #=> [7, 3, 5]
105
- Issue.where(id: ids).order_by_query([[:id, ids]]).to_a #=> [Issue<id=7>, Issue<id=3>, Issue<id=5>]
103
+ Issue.where(id: ids).order_by_query([[:id, ids]]).first(2).to_a #=> [Issue<id=7>, Issue<id=3>]
106
104
  ```
107
105
 
108
106
  ## How it works
@@ -131,8 +129,9 @@ SELECT "posts".* FROM "posts" WHERE
131
129
  "posts"."published_at" < '2014-03-21 15:01:35.064096' OR
132
130
  "posts"."published_at" = '2014-03-21 15:01:35.064096' AND "posts"."id" < 9))
133
131
  ORDER BY
134
- "posts"."pinned"='t' DESC,
135
- "posts"."pinned"='f' DESC, "posts"."published_at" DESC, "posts"."id" DESC
132
+ "posts"."pinned"='t' DESC, "posts"."pinned"='f' DESC,
133
+ "posts"."published_at" DESC,
134
+ "posts"."id" DESC
136
135
  LIMIT 1
137
136
  ```
138
137
 
@@ -158,6 +157,11 @@ ORDER BY
158
157
  LIMIT 1
159
158
  ```
160
159
 
160
+ 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).
162
+
163
+ 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
+
161
165
  This project uses MIT license.
162
166
 
163
167
 
@@ -2,12 +2,12 @@ module OrderQuery
2
2
  class OrderCondition
3
3
  attr_reader :name, :order, :order_order, :options, :scope
4
4
 
5
- def initialize(scope, line)
6
- line = line.dup
7
- @options = line.extract_options!
8
- @name = line[0]
9
- @order = line[1] || :asc
10
- @order_order = line[2] || :desc
5
+ 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
11
  @scope = scope
12
12
  @unique = @options.key?(:unique) ? !!@options[:unique] : (name.to_s == scope.primary_key)
13
13
  end
@@ -16,6 +16,29 @@ module OrderQuery
16
16
  @unique
17
17
  end
18
18
 
19
+ def ray?
20
+ !order.is_a?(Array)
21
+ end
22
+
23
+ # @param [Object] value
24
+ # @param [:before, :after] mode
25
+ # @return [Array] valid order values before / after passed (depending on the mode)
26
+ def values_around(value, mode, strict = true)
27
+ ord = order
28
+ pos = ord.index(value)
29
+ if pos
30
+ dir = order_order
31
+ if mode == :after && dir == :desc || mode == :before && dir == :asc
32
+ ord.from pos + (strict ? 1 : 0)
33
+ else
34
+ ord.first pos + (strict ? 0 : 1)
35
+ end
36
+ else
37
+ # default to all if current is not in sort order values
38
+ ord
39
+ end
40
+ end
41
+
19
42
  def col_name_sql
20
43
  sql = options[:sql]
21
44
  if sql
@@ -1,44 +1,74 @@
1
1
  require 'order_query/order_condition'
2
2
  module OrderQuery
3
+ # Combine order specification with a scope
3
4
  class OrderSpace
4
- include Enumerable
5
- attr_reader :order
5
+ attr_reader :conditions
6
6
 
7
- delegate :each, :length, :size, to: :@order
8
-
9
- def initialize(scope, order)
7
+ # @param [ActiveRecord::Relation] scope
8
+ # @param [Array<Array<Symbol,String>>] order_spec
9
+ def initialize(scope, order_spec)
10
10
  @scope = scope
11
- @order = order.map { |line| OrderCondition.new(scope, line) }
11
+ @conditions = order_spec.map { |spec| OrderCondition.new(scope, spec) }
12
12
  end
13
13
 
14
+ # @return [ActiveRecord::Relation]
14
15
  def scope
15
16
  @scope.order(order_by_sql)
16
17
  end
17
18
 
19
+ # @return [ActiveRecord::Relation]
18
20
  def reverse_scope
19
21
  @scope.order(order_by_reverse_sql)
20
22
  end
21
23
 
22
- def to_order_by_sql
23
- @order.map { |spec|
24
- ord = spec.order
25
- if ord == :asc || ord == :desc
26
- "#{spec.col_name_sql} #{ord.to_s.upcase}"
27
- elsif ord.respond_to?(:map)
28
- ord.map { |v| "#{spec.col_name_sql}=#{@scope.connection.quote v} #{spec.order_order.to_s.upcase}" } * ', '
29
- else
30
- raise "Unknown order #{spec.order.inspect} (#{spec.inspect})"
24
+ SORT_DIRECTIONS = [:asc, :desc].freeze
25
+
26
+ # @return [String]
27
+ def sort_direction_sql(direction)
28
+ if SORT_DIRECTIONS.include?(direction)
29
+ direction.to_s.upcase.freeze
30
+ else
31
+ raise ArgumentError.new("sort direction must be in #{SORT_DIRECTIONS.map(&:inspect).join(', ')}, is #{direction.inspect}")
32
+ end
33
+ end
34
+
35
+ # @return [Array<String>]
36
+ def order_by_sql_clauses
37
+ conditions.map { |cond|
38
+ case order_spec = cond.order
39
+ when Symbol
40
+ "#{cond.col_name_sql} #{sort_direction_sql order_spec}".freeze
41
+ when Enumerable
42
+ order_spec.map { |v|
43
+ "#{cond.col_name_sql}=#{@scope.connection.quote v} #{cond.order_order.to_s.upcase}"
44
+ }.join(', ').freeze
45
+ else
46
+ raise ArgumentError.new("Invalid order #{order_spec.inspect} (#{cond.inspect})")
31
47
  end
32
48
  }
33
49
  end
34
50
 
35
- def order_by_reverse_sql
51
+ # @return [Array<String>]
52
+ def order_by_reverse_sql_clauses
36
53
  swap = {'DESC' => 'ASC', 'ASC' => 'DESC'}
37
- to_order_by_sql.map { |s| s.gsub(/DESC|ASC/) { |m| swap[m] } } * ', '
54
+ order_by_sql_clauses.map { |s|
55
+ s.gsub(/DESC|ASC/) { |m| swap[m] }
56
+ }
38
57
  end
39
58
 
59
+ # @return [String]
60
+ def order_by_reverse_sql
61
+ join_order_by_clauses order_by_reverse_sql_clauses
62
+ end
63
+
64
+ # @return [String]
40
65
  def order_by_sql
41
- to_order_by_sql * ', '
66
+ join_order_by_clauses order_by_sql_clauses
67
+ end
68
+
69
+ # @param [Array<String>] clauses
70
+ def join_order_by_clauses(clauses)
71
+ clauses.join(', ').freeze
42
72
  end
43
73
  end
44
74
  end
@@ -1,50 +1,67 @@
1
1
  require 'order_query/order_space'
2
+ require 'order_query/where_builder'
3
+
2
4
  module OrderQuery
3
5
 
6
+ # Search around a record in a scope
4
7
  class RelativeOrder
5
- attr_reader :record, :scope, :order, :values, :options
8
+ attr_reader :record, :order
9
+ delegate :scope, :reverse_scope, to: :order
6
10
 
7
- def initialize(record, scope, order)
11
+ # @param [ActiveRecord::Base] record
12
+ # @param [OrderQuery::OrderSpace] order_space
13
+ def initialize(record, order_space)
8
14
  @record = record
9
- @scope = scope
10
- @order = order.is_a?(OrderSpace) ? order : OrderSpace.new(scope, order)
15
+ @order = order_space
16
+ @query_builder = WhereBuilder.new record, order_space
11
17
  end
12
18
 
19
+ # @return [ActiveRecord::Base]
13
20
  def first
14
- order.scope.first
21
+ scope.first
15
22
  end
16
23
 
24
+ # @return [ActiveRecord::Base]
17
25
  def last
18
- order.scope.last
26
+ reverse_scope.first
19
27
  end
20
28
 
29
+ # @return [Integer]
21
30
  def count
22
31
  @total ||= scope.count
23
32
  end
24
33
 
34
+ # @return [Integer]
25
35
  def position
26
36
  count - after.count
27
37
  end
28
38
 
39
+ # @params [true, false] loop if true, consider last and first as adjacent (unless they are equal)
40
+ # @return [ActiveRecord::Base]
29
41
  def next(loop = true)
30
- record_unless_current after.first || (first if loop)
42
+ unless_record_eq after.first || (first if loop)
31
43
  end
32
44
 
45
+ # @return [ActiveRecord::Base]
33
46
  def previous(loop = true)
34
- record_unless_current before.first || (last if loop)
47
+ unless_record_eq before.first || (last if loop)
35
48
  end
36
49
 
50
+ # @return [ActiveRecord::Relation]
37
51
  def after
38
52
  records :after
39
53
  end
40
54
 
55
+ # @return [ActiveRecord::Relation]
41
56
  def before
42
57
  records :before
43
58
  end
44
59
 
45
- def records(mode)
46
- scope = (mode == :after ? order.scope : order.reverse_scope)
47
- query, query_args = build_query(mode)
60
+ # @param [:before, :after] direction
61
+ # @return [ActiveRecord::Relation]
62
+ def records(direction)
63
+ scope = (direction == :after ? order.scope : order.reverse_scope)
64
+ query, query_args = @query_builder.build_query(direction)
48
65
  if query.present?
49
66
  scope.where(query, *query_args)
50
67
  else
@@ -54,95 +71,10 @@ module OrderQuery
54
71
 
55
72
  protected
56
73
 
57
- def record_unless_current(record)
58
- record unless record == @record
59
- end
60
-
61
- # @param [:before or :after] mode
62
- # @return [query, parameters] conditions that exclude all elements not before / after the current one
63
- def build_query(mode)
64
- group_operators order.map { |term| [where_mode(term, mode), where_eq(term)] }
65
- end
66
-
67
- # Join conditions with operators and parenthesis
68
- # @param [Array] term_pairs of query terms [[x0, y0], [x1, y1], ...],
69
- # xi, yi are pairs of [query, parameters]
70
- # @return [query, parameters]
71
- # x0 OR
72
- # y0 AND (x1 OR
73
- # y1 AND (x2 OR
74
- # y2 AND x3))
75
- #
76
- # Since x matches order criteria with values that come before / after the current record,
77
- # and y matches order criteria with values equal to the current record's value (for resolving ties),
78
- # the resulting condition matches just the elements that come before / after the record
79
- def group_operators(term_pairs)
80
- # create "x OR y" string
81
- term = join_terms 'OR', *term_pairs[0]
82
- rest = term_pairs.from(1)
83
- if rest.present?
84
- # nest the remaining pairs recursively, appending them with " AND "
85
- rest_grouped = group_operators rest
86
- rest_grouped[0] = "(#{rest_grouped[0]})" unless rest.length == 1
87
- join_terms 'AND', term, rest_grouped
88
- else
89
- term
90
- end
91
- end
92
-
93
- # joins terms with an operator
94
- # @return [query, parameters]
95
- def join_terms(op, *terms)
96
- [terms.map { |t| t.first.presence }.compact.join(" #{op} "),
97
- terms.map(&:second).reduce(:+) || []]
98
- end
99
-
100
- EMPTY_FILTER = ['', []]
101
-
102
- # @return [query, params] Unless order attribute is unique, such as id, return ['WHERE value = ?', current value].
103
- def where_eq(attr)
104
- if attr.unique?
105
- EMPTY_FILTER
106
- else
107
- [%Q(#{attr.col_name_sql} = ?), [attr_value(attr)]]
108
- end
109
- end
110
-
111
- # @param [:before or :after] mode
112
- # @return [query, params] return query conditions for attribute values before / after the current one
113
- def where_mode(attr, mode)
114
- ord = attr.order
115
- value = attr_value attr
116
- if ord.is_a?(Array)
117
- # ord is an array of sort values, ordered first to last
118
- pos = ord.index(value)
119
- sort_values = if pos
120
- dir = attr.order_order
121
- if mode == :after && dir == :desc || mode == :before && dir == :asc
122
- ord.from(pos + 1)
123
- else
124
- ord.first(pos)
125
- end
126
- else
127
- # default to all if current is not in sort order values
128
- ord
129
- end
130
- # if current not in result set, do not apply filter
131
- return EMPTY_FILTER unless sort_values.present?
132
- if sort_values.length == 1
133
- ["#{attr.col_name_sql} = ?", [sort_values]]
134
- else
135
- ["#{attr.col_name_sql} IN (?)", [sort_values]]
136
- end
137
- else
138
- # ord is :asc or :desc
139
- op = {before: {asc: '<', desc: '>'}, after: {asc: '>', desc: '<'}}[mode][ord || :asc]
140
- ["#{attr.col_name_sql} #{op} ?", [value]]
141
- end
142
- end
143
-
144
- def attr_value(attr)
145
- record.send attr.name
74
+ # @param [ActiveRecord::Base] rec
75
+ # @return [ActiveRecord::Base, nil] rec unless rec == @record
76
+ def unless_record_eq(rec)
77
+ rec unless rec == @record
146
78
  end
147
79
  end
148
80
  end
@@ -1,3 +1,3 @@
1
1
  module OrderQuery
2
- VERSION = '0.1.1'
2
+ VERSION = '0.1.2'
3
3
  end
@@ -0,0 +1,116 @@
1
+ module OrderQuery
2
+ # Build where clause for searching around a record in an order space
3
+ class WhereBuilder
4
+ # @return [ActiveRecord::Base]
5
+ attr_reader :record
6
+ # @return [OrderQuery::OrderSpace]
7
+ attr_reader :order
8
+
9
+ # @param [ActiveRecord::Base] record
10
+ # @param [OrderQuery::OrderSpace] order_space
11
+ def initialize(record, order_space)
12
+ @order = order_space
13
+ @record = record
14
+ end
15
+
16
+ # @param [:before or :after] mode
17
+ # @return [query, parameters] conditions that exclude all elements not before / after the current one
18
+ 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
22
+ # 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]]
27
+ else
28
+ query
29
+ end
30
+ end
31
+
32
+ # Join conditions with operators and parenthesis
33
+ # @param [Array] term_pairs of query terms [[x0, y0], [x1, y1], ...],
34
+ # xi, yi are pairs of [query, parameters]
35
+ # @return [query, parameters]
36
+ # x0 OR
37
+ # y0 AND (x1 OR
38
+ # y1 AND (x2 OR
39
+ # y2 AND x3))
40
+ #
41
+ # Since x matches order criteria with values that come before / after the current record,
42
+ # and y matches order criteria with values equal to the current record's value (for resolving ties),
43
+ # the resulting condition matches just the elements that come before / after the record
44
+ def group_operators(term_pairs)
45
+ # create "x OR y" string
46
+ disjunctive = join_terms 'OR'.freeze, *term_pairs[0]
47
+ rest = term_pairs.from(1)
48
+ if rest.present?
49
+ # 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
53
+ else
54
+ disjunctive
55
+ end
56
+ end
57
+
58
+ # joins terms with an operator
59
+ # @return [query, parameters]
60
+ def join_terms(op, *terms)
61
+ [terms.map { |t| t.first.presence }.compact.join(" #{op} "),
62
+ terms.map(&:second).reduce(:+) || []]
63
+ end
64
+
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
71
+ else
72
+ [%Q(#{cond.col_name_sql} = ?).freeze, [attr_value(cond)]]
73
+ end
74
+ end
75
+
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
+
83
+ def where_in(cond, values)
84
+ case values.length
85
+ when 0
86
+ EMPTY_FILTER
87
+ when 1
88
+ ["#{cond.col_name_sql} = ?".freeze, [values]]
89
+ else
90
+ ["#{cond.col_name_sql} IN (?)".freeze, [values]]
91
+ end
92
+ end
93
+
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
105
+ end
106
+
107
+ def attr_value(cond)
108
+ record.send cond.name
109
+ end
110
+
111
+ class << self
112
+ attr_accessor :wrap_top_level_or
113
+ end
114
+ self.wrap_top_level_or = true
115
+ end
116
+ end
data/lib/order_query.rb CHANGED
@@ -6,19 +6,20 @@ module OrderQuery
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- # @return [OrderSpace] order definition
10
- # @example
11
- # Issue.order_query [[:id, :desc]] #=> <ActiveRecord::Relation#...>
12
- scope :order_by_query, ->(order) { OrderSpace.new(self, order).scope }
13
- scope :reverse_order_by_query, ->(order) { OrderSpace.new(self, order).reverse_scope }
9
+ scope :order_by_query, ->(order_spec) { OrderSpace.new(self, order_spec).scope }
10
+ scope :reverse_order_by_query, ->(order_spec) { OrderSpace.new(self, order_spec).reverse_scope }
14
11
  end
15
12
 
16
- def relative_order_by_query(scope = self.class.all, order)
17
- RelativeOrder.new(self, scope, order)
13
+ # @param [ActiveRecord::Relation] scope
14
+ # @param [Array<Array<Symbol,String>>] order_spec
15
+ def relative_order_by_query(scope = self.class.all, order_spec)
16
+ RelativeOrder.new(self, OrderSpace.new(scope, order_spec))
18
17
  end
19
18
 
20
19
  module ClassMethods
21
20
  protected
21
+ # @param [Symbol] name
22
+ # @param [Array<Array<Symbol,String>>] order_spec
22
23
  # @example
23
24
  # class Issue
24
25
  # order_query :order_display, [[:created_at, :desc], [:id, :desc]]
@@ -26,12 +27,11 @@ module OrderQuery
26
27
  #
27
28
  # Issue.order_display #=> <ActiveRecord::Relation#...>
28
29
  # Issue.active.find(31).display_order(Issue.active).next #=> <Issue#...>
29
- def order_query(name, order)
30
- scope name, -> { order_by_query(order) }
31
- scope :"reverse_#{name}", -> { reverse_order_by_query(order) }
32
- define_method(name) do |scope = nil|
33
- scope ||= self.class.all
34
- relative_order_by_query(scope, order)
30
+ def order_query(name, order_spec)
31
+ scope name, -> { order_by_query(order_spec) }
32
+ scope :"reverse_#{name}", -> { reverse_order_by_query(order_spec) }
33
+ define_method name do |scope = nil|
34
+ relative_order_by_query scope || self.class.all, order_spec
35
35
  end
36
36
  end
37
37
  end
@@ -36,127 +36,153 @@ def create_issue(attr = {})
36
36
  Issue.create!({priority: 'high', votes: 3, suspicious_votes: 0, updated_at: Time.now}.merge(attr))
37
37
  end
38
38
 
39
- describe 'OrderQuery.order_query' do
40
-
41
- context 'Issue test model' do
42
- t = Time.now
43
- datasets = [
44
- [
45
- ['high', 5, 0, t],
46
- ['high', 5, 1, t],
47
- ['high', 5, 1, t - 1.day],
48
- ['medium', 10, 0, t],
49
- ['medium', 10, 5, t - 12.hours],
50
- ['low', 30, 0, t + 1.day]
51
- ],
52
- [
53
- ['high', 5, 0, t],
54
- ['high', 5, 1, t],
55
- ['high', 5, 1, t - 1.day],
56
- ['low', 30, 0, t + 1.day]
57
- ],
58
- [
59
- ['high', 5, 1, t - 1.day],
60
- ['low', 30, 0, t + 1.day]
61
- ],
62
- ]
63
-
64
- datasets.each_with_index do |ds, i|
65
- it "is ordered correctly (test data #{i})" do
66
- issues = ds.map do |attr|
67
- Issue.new(priority: attr[0], votes: attr[1], suspicious_votes: attr[2], updated_at: attr[3])
39
+ def with_wrap_top_level(value)
40
+ builder = OrderQuery::WhereBuilder
41
+ around do |ex|
42
+ was = builder.wrap_top_level_or
43
+ begin
44
+ builder.wrap_top_level_or = value
45
+ ex.run
46
+ ensure
47
+ builder.wrap_top_level_or = was
48
+ end
49
+ end
50
+ end
51
+
52
+ describe 'OrderQuery' do
53
+
54
+ [false, true].each do |wrap_top_level_or|
55
+ context "(wrap_top_level_or: #{wrap_top_level_or})" do
56
+ with_wrap_top_level wrap_top_level_or
57
+
58
+ context 'Issue test model' do
59
+ t = Time.now
60
+ datasets = [
61
+ [
62
+ ['high', 5, 0, t],
63
+ ['high', 5, 1, t],
64
+ ['high', 5, 1, t - 1.day],
65
+ ['medium', 10, 0, t],
66
+ ['medium', 10, 5, t - 12.hours],
67
+ ['low', 30, 0, t + 1.day]
68
+ ],
69
+ [
70
+ ['high', 5, 0, t],
71
+ ['high', 5, 1, t],
72
+ ['high', 5, 1, t - 1.day],
73
+ ['low', 30, 0, t + 1.day]
74
+ ],
75
+ [
76
+ ['high', 5, 1, t - 1.day],
77
+ ['low', 30, 0, t + 1.day]
78
+ ],
79
+ ]
80
+
81
+ datasets.each_with_index do |ds, i|
82
+ it "is ordered correctly (test data #{i})" do
83
+ issues = ds.map do |attr|
84
+ Issue.new(priority: attr[0], votes: attr[1], suspicious_votes: attr[2], updated_at: attr[3])
85
+ end
86
+ issues.reverse_each(&:save!)
87
+ expect(Issue.display_order.to_a).to eq(issues)
88
+ issues.each_slice(2) do |prev, cur|
89
+ cur ||= issues.first
90
+ expect(prev.display_order.next).to eq(cur)
91
+ expect(cur.display_order.previous).to eq(prev)
92
+ expect(cur.display_order.scope.count).to eq(Issue.count)
93
+ expect(cur.display_order.before.count + 1 + cur.display_order.after.count).to eq(cur.display_order.count)
94
+
95
+ expect(cur.display_order.before.to_a.reverse + [cur] + cur.display_order.after.to_a).to eq(Issue.display_order.to_a)
96
+ end
97
+ end
68
98
  end
69
- issues.reverse_each(&:save!)
70
- expect(Issue.display_order.to_a).to eq(issues)
71
- issues.each_slice(2) do |prev, cur|
72
- cur ||= issues.first
73
- expect(prev.display_order.next).to eq(cur)
74
- expect(cur.display_order.previous).to eq(prev)
75
- expect(cur.display_order.scope.count).to eq(Issue.count)
76
- expect(cur.display_order.before.count + 1 + cur.display_order.after.count).to eq(cur.display_order.count)
77
-
78
- expect(cur.display_order.before.to_a.reverse + [cur] + cur.display_order.after.to_a).to eq(Issue.display_order.to_a)
99
+
100
+ it '#next returns nil when there is only 1 record' do
101
+ p = create_issue.display_order
102
+ expect(p.next).to be_nil
103
+ expect(p.next(true)).to be_nil
79
104
  end
80
- end
81
- end
82
105
 
83
- it '#next returns nil when there is only 1 record' do
84
- p = create_issue.display_order
85
- expect(p.next).to be_nil
86
- expect(p.next(true)).to be_nil
87
- end
106
+ it 'is ordered correctly for order query [[:id, :asc]]' do
107
+ a = create_issue
108
+ b = create_issue
109
+ expect(a.id_order_asc.next).to eq b
110
+ expect(b.id_order_asc.previous).to eq a
111
+ expect([a] + a.id_order_asc.after.to_a).to eq(Issue.id_order_asc.to_a)
112
+ expect(b.id_order_asc.before.reverse.to_a + [b]).to eq(Issue.id_order_asc.to_a)
113
+ expect(Issue.id_order_asc.count).to eq(2)
114
+ end
88
115
 
89
- it 'is ordered correctly for order query [[:id, :asc]]' do
90
- a = create_issue
91
- b = create_issue
92
- expect(a.id_order_asc.next).to eq b
93
- expect(b.id_order_asc.previous).to eq a
94
- expect([a] + a.id_order_asc.after.to_a).to eq(Issue.id_order_asc.to_a)
95
- expect(b.id_order_asc.before.reverse.to_a + [b]).to eq(Issue.id_order_asc.to_a)
96
- expect(Issue.id_order_asc.count).to eq(2)
97
- end
116
+ it '.order_by_query works on a list of ids' do
117
+ ids = (1..3).map { create_issue.id }
118
+ expect(Issue.order_by_query([[:id, ids]]).size).to eq ids.length
119
+ end
98
120
 
99
- it '.order_by_query works on a list of ids' do
100
- ids = (1..3).map { create_issue.id }
101
- expect(Issue.order_by_query([[:id, ids]])).to have(ids.length).issues
102
- end
121
+ it '.order_by_query preserves previous' do
122
+ create_issue(active: true)
123
+ expect(Issue.where(active: false).order_by_query([[:id, :desc]])).to be_empty
124
+ expect(Issue.where(active: true).order_by_query([[:id, :desc]]).size).to eq 1
125
+ end
103
126
 
104
- it '.order_by_query preserves previous' do
105
- create_issue(active: true)
106
- expect(Issue.where(active: false).order_by_query([[:id, :desc]])).to have(0).records
107
- expect(Issue.where(active: true).order_by_query([[:id, :desc]])).to have(1).record
108
- end
127
+ it '#relative_order_by_query falls back to scope when order condition is missing self' do
128
+ a = create_issue(priority: 'medium')
129
+ b = create_issue(priority: 'high')
130
+ expect(a.relative_order_by_query(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
131
+ end
109
132
 
110
- it '#relative_order_by_query falls back to scope when order condition is missing self' do
111
- a = create_issue(priority: 'medium')
112
- b = create_issue(priority: 'high')
113
- expect(a.relative_order_by_query(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
114
- end
133
+ before do
134
+ Issue.delete_all
135
+ end
115
136
 
116
- before do
117
- Issue.delete_all
118
- end
137
+ before :all do
138
+ ActiveRecord::Schema.define do
139
+ self.verbose = false
140
+
141
+ create_table :issues do |t|
142
+ t.column :priority, :string
143
+ t.column :votes, :integer
144
+ t.column :suspicious_votes, :integer
145
+ t.column :announced_at, :datetime
146
+ t.column :updated_at, :datetime
147
+ t.column :active, :boolen, null: false, default: true
148
+ end
149
+ end
150
+
151
+ Issue.reset_column_information
152
+ end
119
153
 
120
- before :all do
121
- ActiveRecord::Schema.define do
122
- self.verbose = false
123
-
124
- create_table :issues do |t|
125
- t.column :priority, :string
126
- t.column :votes, :integer
127
- t.column :suspicious_votes, :integer
128
- t.column :announced_at, :datetime
129
- t.column :updated_at, :datetime
130
- t.column :active, :boolen, null: false, default: true
154
+ after :all do
155
+ ActiveRecord::Migration.drop_table :issues
131
156
  end
132
157
  end
133
158
 
134
- Issue.reset_column_information
135
- end
136
- end
137
-
138
- context 'Post test model' do
139
- it '#next works' do
140
- p1 = create_post(pinned: true)
141
- o1 = p1.order_list
142
- expect(o1.next).to be_nil
143
- expect(o1.next(true)).to be_nil
144
- p2 = create_post(pinned: false)
145
- o2 = p2.order_list
146
- expect(o1.next(false)).to eq(p2)
147
- expect(o2.next(false)).to be_nil
148
- expect(o2.next(true)).to eq(p1)
149
- end
159
+ context 'Post test model' do
160
+ it '#next works' do
161
+ p1 = create_post(pinned: true)
162
+ o1 = p1.order_list
163
+ expect(o1.next).to be_nil
164
+ expect(o1.next(true)).to be_nil
165
+ p2 = create_post(pinned: false)
166
+ o2 = p2.order_list
167
+ expect(o1.next(false)).to eq(p2)
168
+ expect(o2.next(false)).to be_nil
169
+ expect(o2.next(true)).to eq(p1)
170
+ end
150
171
 
151
- before do
152
- Post.delete_all
153
- end
154
- before :all do
155
- ActiveRecord::Schema.define do
156
- self.verbose = false
157
- create_table :posts do |t|
158
- t.boolean :pinned
159
- t.datetime :published_at
172
+ before do
173
+ Post.delete_all
174
+ end
175
+ before :all do
176
+ ActiveRecord::Schema.define do
177
+ self.verbose = false
178
+ create_table :posts do |t|
179
+ t.boolean :pinned
180
+ t.datetime :published_at
181
+ end
182
+ end
183
+ end
184
+ after :all do
185
+ ActiveRecord::Migration.drop_table :posts
160
186
  end
161
187
  end
162
188
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: order_query
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Mazovetskiy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-25 00:00:00.000000000 Z
11
+ date: 2014-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.14'
47
+ version: '3.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.14'
54
+ version: '3.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -82,6 +82,7 @@ files:
82
82
  - lib/order_query/order_space.rb
83
83
  - lib/order_query/relative_order.rb
84
84
  - lib/order_query/version.rb
85
+ - lib/order_query/where_builder.rb
85
86
  - spec/order_query_spec.rb
86
87
  - spec/spec_helper.rb
87
88
  homepage: https://github.com/glebm/order_query
@@ -105,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
106
  version: '0'
106
107
  requirements: []
107
108
  rubyforge_project:
108
- rubygems_version: 2.2.0
109
+ rubygems_version: 2.4.1
109
110
  signing_key:
110
111
  specification_version: 4
111
112
  summary: Find next / previous Active Record(s) in one query