order_query 0.1.1 → 0.1.2

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