order_query 0.1.3 → 0.2.0

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: b77ab79b198ad089f382ace5d234c433f239bff0
4
- data.tar.gz: 1c2507543963747b645dd4426328fea48b6909e1
3
+ metadata.gz: 14a1cc9219fd6bb2221967eef338dcabee8854f1
4
+ data.tar.gz: e4a8c908c803e2c8afad23a0b278473043087cc7
5
5
  SHA512:
6
- metadata.gz: 258a2dd941e882fb115a1a15f1f2be6cabe359840d9fe06fbbb6fbbe41dd37284b4cc27909507624367412a65383d6480462cb37d19181acff1602a8c67894b0
7
- data.tar.gz: d72f77b66611f6ca0f784ac080e1ef6cd99369fe4cf8c279c8a7bdaa3a130a13821df4efceb035ff201772d7f3cde0563b1dac4752b95d6feaee69bb0fd9e624
6
+ metadata.gz: e93eb1ec39fab70c1895d34aeef9bbfab92c8da4b7c2962c7a822f4e0df5501331c4a5c733a471fdcfb61b287d4e9cebfd8ea668b258d957755e942011384ee1
7
+ data.tar.gz: 4ec32294d12907c1245607f2f2f79a9f205e9f7c7d7396f4b7a1bd1877fb597262cf29e7aa53d81aad512efc36a9737dbf68601f9828f69b7b16adaa77dc77eb
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.2.0
2
+
3
+ * Dynamic query methods renamed to `order_by`
4
+
1
5
  ## 0.1.3
2
6
 
3
7
  * New condition option `complete` for list conditions for optimized query generation
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.3'
19
+ gem 'order_query', '~> 0.2.0'
20
20
  ```
21
21
 
22
22
  ## Usage
@@ -94,20 +94,20 @@ end
94
94
 
95
95
  ### Dynamic order conditions
96
96
 
97
- To query with dynamic order conditions use `Model.order_by_query` and `Model#relative_order_by_query`:
97
+ To query with dynamic order conditions use `Model.order_by` and `Model#order_by`:
98
98
 
99
99
  ```ruby
100
- Issue.order_by_query([[:id, :desc]]) #=> ActiveRecord::Relation<...>
101
- Issue.reverse_order_by_query([[:id, :desc]]) #=> ActiveRecord::Relation<...>
102
- Issue.find(31).relative_order_by_query([[:id, :desc]]).next #=> Issue<...>
103
- Issue.find(31).relative_order_by_query(Issue.visible, [[:id, :desc]]).next #=> Issue<...>
100
+ Issue.order_by([[:id, :desc]]) #=> ActiveRecord::Relation<...>
101
+ Issue.visible.reverse_order_by([[:id, :desc]]) #=> ActiveRecord::Relation<...>
102
+ Issue.find(31).order_by([[:id, :desc]]).next #=> Issue<...>
103
+ Issue.find(31).order_by(Issue.visible, [[:id, :desc]]).next #=> Issue<...>
104
104
  ```
105
105
 
106
- For example, consider ordering by a list of ids returned from an elasticsearh query:
106
+ For example, consider ordering by a list of ids returned from an elasticsearch query:
107
107
 
108
108
  ```ruby
109
109
  ids = Issue.keyword_search('ruby') #=> [7, 3, 5]
110
- Issue.where(id: ids).order_by_query([[:id, ids]]).first(2).to_a #=> [Issue<id=7>, Issue<id=3>]
110
+ Issue.where(id: ids).order_by([[:id, ids]]).first(2).to_a #=> [Issue<id=7>, Issue<id=3>]
111
111
  ```
112
112
 
113
113
  ## How it works
data/lib/order_query.rb CHANGED
@@ -1,22 +1,28 @@
1
1
  require 'active_support'
2
2
  require 'active_record'
3
- require 'order_query/relative_order'
3
+ require 'order_query/space'
4
+ require 'order_query/point'
4
5
 
5
6
  module OrderQuery
6
7
  extend ActiveSupport::Concern
7
8
 
8
- included do
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 }
11
- end
12
-
13
9
  # @param [ActiveRecord::Relation] scope
14
10
  # @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))
11
+ def order_by(scope = nil, order_spec)
12
+ scope ||= self.class.all
13
+ Point.new(self, Space.new(scope, order_spec))
17
14
  end
18
15
 
19
16
  module ClassMethods
17
+ def order_by(order_spec)
18
+ Space.new(self, order_spec).scope
19
+ end
20
+
21
+ def reverse_order_by(order_spec)
22
+ Space.new(self, order_spec).reverse_scope
23
+ end
24
+
25
+ #= DSL
20
26
  protected
21
27
  # @param [Symbol] name
22
28
  # @param [Array<Array<Symbol,String>>] order_spec
@@ -28,11 +34,17 @@ module OrderQuery
28
34
  # Issue.order_display #=> <ActiveRecord::Relation#...>
29
35
  # Issue.active.find(31).display_order(Issue.active).next #=> <Issue#...>
30
36
  def order_query(name, order_spec)
31
- scope name, -> { order_by_query(order_spec) }
32
- scope :"reverse_#{name}", -> { reverse_order_by_query(order_spec) }
37
+ scope name, -> { order_by(order_spec) }
38
+ scope :"reverse_#{name}", -> { reverse_order_by(order_spec) }
33
39
  define_method name do |scope = nil|
34
- relative_order_by_query scope || self.class.all, order_spec
40
+ order_by scope, order_spec
35
41
  end
36
42
  end
37
43
  end
44
+
45
+ class << self
46
+ attr_accessor :wrap_top_level_or
47
+ end
48
+ # Wrap top-level or with an AND and a redundant condition for performance
49
+ self.wrap_top_level_or = true
38
50
  end
@@ -0,0 +1,65 @@
1
+ require 'order_query/sql/condition'
2
+ module OrderQuery
3
+ class Condition
4
+ attr_reader :name, :order, :order_enum, :options, :sql
5
+
6
+ # @option spec [String] :unique Mark the attribute as unique to avoid redundant conditions
7
+ # @option spec [String] :complete Mark the condition's domain as complete to avoid redundant conditions (only for array conditions)
8
+ def initialize(spec, scope)
9
+ spec = spec.dup
10
+ options = spec.extract_options!
11
+ @name = spec[0]
12
+ case spec[1]
13
+ when Array
14
+ @order_enum = spec[1]
15
+ @order = spec[2] || :desc
16
+ else
17
+ @order = spec[1] || :asc
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
+
31
+ @sql = SQL::Condition.new(self, scope)
32
+ end
33
+
34
+ def unique?
35
+ @unique
36
+ end
37
+
38
+ def complete?
39
+ @complete
40
+ end
41
+
42
+ # @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)
46
+ ord = order_enum
47
+ pos = ord.index(value)
48
+ if pos
49
+ dir = order
50
+ if mode == :after && dir == :desc || mode == :before && dir == :asc
51
+ ord.from pos + (strict ? 1 : 0)
52
+ else
53
+ ord.first pos + (strict ? 0 : 1)
54
+ end
55
+ else
56
+ # default to all if current is not in sort order values
57
+ ord
58
+ end
59
+ end
60
+
61
+ def inspect
62
+ "Condition(#{@name.inspect}#{" #{@order_enum.inspect}" if @order_enum} #{@order.to_s.upcase} #{'unique ' if @unique}#{@complete ? 'complete' : 'partial' if @order_enum})"
63
+ end
64
+ end
65
+ end
@@ -1,19 +1,18 @@
1
- require 'order_query/order_space'
2
- require 'order_query/where_builder'
1
+ require 'order_query/space'
2
+ require 'order_query/sql/where'
3
3
 
4
4
  module OrderQuery
5
-
6
- # Search around a record in a scope
7
- class RelativeOrder
8
- attr_reader :record, :order
9
- delegate :scope, :reverse_scope, to: :order
5
+ # Search around a record in an order space
6
+ class Point
7
+ attr_reader :record, :space
8
+ delegate :scope, :reverse_scope, to: :space
10
9
 
11
10
  # @param [ActiveRecord::Base] record
12
- # @param [OrderQuery::OrderSpace] order_space
13
- def initialize(record, order_space)
14
- @record = record
15
- @order = order_space
16
- @query_builder = WhereBuilder.new record, order_space
11
+ # @param [OrderQuery::Space] space
12
+ def initialize(record, space)
13
+ @record = record
14
+ @space = space
15
+ @where_sql = SQL::Where.new(self)
17
16
  end
18
17
 
19
18
  # @return [ActiveRecord::Base]
@@ -60,8 +59,8 @@ module OrderQuery
60
59
  # @param [:before, :after] direction
61
60
  # @return [ActiveRecord::Relation]
62
61
  def records(direction)
63
- scope = (direction == :after ? order.scope : order.reverse_scope)
64
- query, query_args = @query_builder.build_query(direction)
62
+ query, query_args = @where_sql.build(direction)
63
+ scope = (direction == :after ? space.scope : space.reverse_scope)
65
64
  if query.present?
66
65
  scope.where(query, *query_args)
67
66
  else
@@ -0,0 +1,30 @@
1
+ require 'order_query/condition'
2
+ require 'order_query/sql/order_by'
3
+ module OrderQuery
4
+ # Order specification and a scope
5
+ class Space
6
+ # @return [Array<Condition>]
7
+ attr_reader :conditions
8
+ # @return [ActiveRecord::Relation]
9
+ attr_reader :base_scope
10
+
11
+ # @param [ActiveRecord::Relation] scope
12
+ # @param [Array<Array<Symbol,String>>] order_spec
13
+ def initialize(base_scope, order_spec)
14
+ @base_scope = base_scope
15
+ order_spec = [order_spec] unless order_spec.empty? || order_spec.first.is_a?(Array)
16
+ @conditions = order_spec.map { |spec| Condition.new(spec, base_scope) }
17
+ @order_by_sql = SQL::OrderBy.new(self)
18
+ end
19
+
20
+ # @return [ActiveRecord::Relation] scope ordered by conditions
21
+ def scope
22
+ @base_scope.order(@order_by_sql.build)
23
+ end
24
+
25
+ # @return [ActiveRecord::Relation] scope ordered by conditions in reverse
26
+ def reverse_scope
27
+ @base_scope.order(@order_by_sql.build_reverse)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ module OrderQuery
2
+ module SQL
3
+ class Condition
4
+ attr_reader :condition, :scope
5
+
6
+ def initialize(condition, scope)
7
+ @condition = condition
8
+ @scope = scope
9
+ end
10
+
11
+ def column_name
12
+ @column_name ||= begin
13
+ sql = condition.options[:sql]
14
+ if sql
15
+ sql = sql.call if sql.respond_to?(:call)
16
+ sql
17
+ else
18
+ connection.quote_table_name(scope.table_name) + '.' + connection.quote_column_name(condition.name)
19
+ end
20
+ end
21
+ end
22
+
23
+ def quote(value)
24
+ connection.quote value
25
+ end
26
+
27
+ protected
28
+
29
+ def connection
30
+ scope.connection
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ module OrderQuery
2
+ module SQL
3
+ class OrderBy
4
+ attr_reader :space
5
+
6
+ # @param [Array<Condition>]
7
+ def initialize(space)
8
+ @space = space
9
+ end
10
+
11
+ # @return [String]
12
+ def build
13
+ join_order_by_clauses order_by_sql_clauses
14
+ end
15
+
16
+ # @return [String]
17
+ def build_reverse
18
+ join_order_by_clauses order_by_reverse_sql_clauses
19
+ end
20
+
21
+ protected
22
+
23
+ # @return [Array<String>]
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
+ }
34
+ end
35
+
36
+ SORT_DIRECTIONS = [:asc, :desc].freeze
37
+ # @return [String]
38
+ def sort_direction_sql(direction)
39
+ if SORT_DIRECTIONS.include?(direction)
40
+ direction.to_s.upcase.freeze
41
+ else
42
+ raise ArgumentError.new("sort direction must be in #{SORT_DIRECTIONS.map(&:inspect).join(', ')}, is #{direction.inspect}")
43
+ end
44
+ end
45
+
46
+ # @param [Array<String>] clauses
47
+ def join_order_by_clauses(clauses)
48
+ clauses.join(', ').freeze
49
+ end
50
+
51
+ # @return [Array<String>]
52
+ def order_by_reverse_sql_clauses
53
+ swap = {'DESC' => 'ASC', 'ASC' => 'DESC'}
54
+ order_by_sql_clauses.map { |s|
55
+ s.gsub(/DESC|ASC/) { |m| swap[m] }
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,120 @@
1
+ # coding: utf-8
2
+ module OrderQuery
3
+ module SQL
4
+ # Build where clause for searching around a record in an order space
5
+ class Where
6
+ attr_reader :point
7
+
8
+ # @param [OrderQuery::Point] point
9
+ def initialize(point)
10
+ @point = point
11
+ end
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
22
+ }
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
+ top_pair_idx = pairs.index(&:present?)
27
+ if top_pair_idx &&
28
+ (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)
32
+ else
33
+ query
34
+ end
35
+ end
36
+
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))
57
+ else
58
+ disjunctive
59
+ end
60
+ end
61
+
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
74
+ # @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
77
+ if cond.order_enum
78
+ values = cond.filter_values(value, mode, strict)
79
+ if cond.complete? && values.length == cond.order_enum.length
80
+ WHERE_IDENTITY
81
+ else
82
+ where_in cond, values
83
+ end
84
+ else
85
+ where_ray cond, value, mode, strict
86
+ end
87
+ end
88
+
89
+
90
+ def where_in(cond, values)
91
+ case values.length
92
+ when 0
93
+ WHERE_NONE
94
+ when 1
95
+ where_eq cond, values[0]
96
+ else
97
+ ["#{cond.sql.column_name} IN (?)".freeze, [values]]
98
+ end
99
+ end
100
+
101
+ def where_eq(cond, value = attr_value(cond))
102
+ [%Q(#{cond.sql.column_name} = ?).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.sql.column_name} #{op}#{'=' unless strict} ?".freeze, [from]]
110
+ end
111
+
112
+ WHERE_IDENTITY = [''.freeze, [].freeze].freeze
113
+ WHERE_NONE = ['∅'.freeze, [].freeze].freeze
114
+
115
+ def attr_value(cond)
116
+ point.record.send cond.name
117
+ end
118
+ end
119
+ end
120
+ end
@@ -1,3 +1,3 @@
1
1
  module OrderQuery
2
- VERSION = '0.1.3'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -1,6 +1,11 @@
1
1
  require 'spec_helper'
2
2
 
3
- # Simple example
3
+ # Bare model
4
+ class User < ActiveRecord::Base
5
+ include OrderQuery
6
+ end
7
+
8
+ # Simple model
4
9
  class Post < ActiveRecord::Base
5
10
  include OrderQuery
6
11
  order_query :order_list, [
@@ -14,7 +19,7 @@ def create_post(attr = {})
14
19
  Post.create!({pinned: false, published_at: Time.now}.merge(attr))
15
20
  end
16
21
 
17
- # Advanced example
22
+ # Advanced model
18
23
  class Issue < ActiveRecord::Base
19
24
  DISPLAY_ORDER = [
20
25
  [:priority, %w(high medium low), complete: true],
@@ -36,15 +41,15 @@ def create_issue(attr = {})
36
41
  Issue.create!({priority: 'high', votes: 3, suspicious_votes: 0, updated_at: Time.now}.merge(attr))
37
42
  end
38
43
 
39
- def with_wrap_top_level(value)
40
- builder = OrderQuery::WhereBuilder
44
+ def wrap_top_level_or(value)
45
+ conf = ::OrderQuery
41
46
  around do |ex|
42
- was = builder.wrap_top_level_or
47
+ was = conf.wrap_top_level_or
43
48
  begin
44
- builder.wrap_top_level_or = value
49
+ conf.wrap_top_level_or = value
45
50
  ex.run
46
51
  ensure
47
- builder.wrap_top_level_or = was
52
+ conf.wrap_top_level_or = was
48
53
  end
49
54
  end
50
55
  end
@@ -53,7 +58,7 @@ describe 'OrderQuery' do
53
58
 
54
59
  [false, true].each do |wrap_top_level_or|
55
60
  context "(wrap_top_level_or: #{wrap_top_level_or})" do
56
- with_wrap_top_level wrap_top_level_or
61
+ wrap_top_level_or wrap_top_level_or
57
62
 
58
63
  context 'Issue test model' do
59
64
  t = Time.now
@@ -113,21 +118,21 @@ describe 'OrderQuery' do
113
118
  expect(Issue.id_order_asc.count).to eq(2)
114
119
  end
115
120
 
116
- it '.order_by_query works on a list of ids' do
121
+ it '.order_by works on a list of ids' do
117
122
  ids = (1..3).map { create_issue.id }
118
- expect(Issue.order_by_query([[:id, ids]]).size).to eq ids.length
123
+ expect(Issue.order_by([[:id, ids]]).size).to eq ids.length
119
124
  end
120
125
 
121
- it '.order_by_query preserves previous' do
126
+ it '.order_by preserves previous' do
122
127
  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
128
+ expect(Issue.where(active: false).order_by([[:id, :desc]])).to be_empty
129
+ expect(Issue.where(active: true).order_by([[:id, :desc]]).size).to eq 1
125
130
  end
126
131
 
127
- it '#relative_order_by_query falls back to scope when order condition is missing self' do
132
+ it '#order_by falls back to scope when order condition is missing self' do
128
133
  a = create_issue(priority: 'medium')
129
134
  b = create_issue(priority: 'high')
130
- expect(a.relative_order_by_query(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
135
+ expect(a.order_by(Issue.display_order, [[:priority, ['wontfix', 'askbob']], [:id, :desc]]).next).to eq(b)
131
136
  end
132
137
 
133
138
  before do
@@ -187,4 +192,39 @@ describe 'OrderQuery' do
187
192
  end
188
193
  end
189
194
  end
195
+
196
+ context 'SQL generation' do
197
+ context 'wrap top-level OR on' do
198
+ wrap_top_level_or true
199
+ it 'wraps top-level OR' do
200
+ after_scope = User.create!(updated_at: Date.parse('2014-09-06')).order_by([[:updated_at, :desc], [:id, :desc]]).after
201
+ expect(after_scope.to_sql).to include('<=')
202
+ end
203
+ end
204
+
205
+ context 'wrap top-level OR off' do
206
+ wrap_top_level_or false
207
+ it 'does not wrap top-level OR' do
208
+ after_scope = User.create!(updated_at: Date.parse('2014-09-06')).order_by([[:updated_at, :desc], [:id, :desc]]).after
209
+ expect(after_scope.to_sql).to_not include('<=')
210
+ end
211
+ end
212
+
213
+ before do
214
+ User.delete_all
215
+ end
216
+
217
+ before :all do
218
+ ActiveRecord::Schema.define do
219
+ self.verbose = false
220
+ create_table :users do |t|
221
+ t.datetime :updated_at, null: false
222
+ end
223
+ end
224
+ end
225
+
226
+ after :all do
227
+ ActiveRecord::Migration.drop_table :users
228
+ end
229
+ end
190
230
  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.3
4
+ version: 0.2.0
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-09-04 00:00:00.000000000 Z
11
+ date: 2014-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -78,11 +78,13 @@ files:
78
78
  - README.md
79
79
  - Rakefile
80
80
  - lib/order_query.rb
81
- - lib/order_query/order_condition.rb
82
- - lib/order_query/order_space.rb
83
- - lib/order_query/relative_order.rb
81
+ - lib/order_query/condition.rb
82
+ - lib/order_query/point.rb
83
+ - lib/order_query/space.rb
84
+ - lib/order_query/sql/condition.rb
85
+ - lib/order_query/sql/order_by.rb
86
+ - lib/order_query/sql/where.rb
84
87
  - lib/order_query/version.rb
85
- - lib/order_query/where_builder.rb
86
88
  - spec/order_query_spec.rb
87
89
  - spec/spec_helper.rb
88
90
  homepage: https://github.com/glebm/order_query
@@ -1,71 +0,0 @@
1
- module OrderQuery
2
- class OrderCondition
3
- attr_reader :name, :order, :order_order, :options, :scope
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)
7
- def initialize(scope, spec)
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
25
- end
26
-
27
- def unique?
28
- @unique
29
- end
30
-
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)
38
- end
39
-
40
- # @param [Object] value
41
- # @param [:before, :after] mode
42
- # @return [Array] valid order values before / after passed (depending on the mode)
43
- def filter_values(value, mode, strict = true)
44
- ord = order
45
- pos = ord.index(value)
46
- if pos
47
- dir = order_order
48
- if mode == :after && dir == :desc || mode == :before && dir == :asc
49
- ord.from pos + (strict ? 1 : 0)
50
- else
51
- ord.first pos + (strict ? 0 : 1)
52
- end
53
- else
54
- # default to all if current is not in sort order values
55
- ord
56
- end
57
- end
58
-
59
- def col_name_sql
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
68
- end
69
- end
70
- end
71
- end
@@ -1,74 +0,0 @@
1
- require 'order_query/order_condition'
2
- module OrderQuery
3
- # Combine order specification with a scope
4
- class OrderSpace
5
- attr_reader :conditions
6
-
7
- # @param [ActiveRecord::Relation] scope
8
- # @param [Array<Array<Symbol,String>>] order_spec
9
- def initialize(scope, order_spec)
10
- @scope = scope
11
- @conditions = order_spec.map { |spec| OrderCondition.new(scope, spec) }
12
- end
13
-
14
- # @return [ActiveRecord::Relation]
15
- def scope
16
- @scope.order(order_by_sql)
17
- end
18
-
19
- # @return [ActiveRecord::Relation]
20
- def reverse_scope
21
- @scope.order(order_by_reverse_sql)
22
- end
23
-
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})")
47
- end
48
- }
49
- end
50
-
51
- # @return [Array<String>]
52
- def order_by_reverse_sql_clauses
53
- swap = {'DESC' => 'ASC', 'ASC' => 'DESC'}
54
- order_by_sql_clauses.map { |s|
55
- s.gsub(/DESC|ASC/) { |m| swap[m] }
56
- }
57
- end
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]
65
- def 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
72
- end
73
- end
74
- end
@@ -1,124 +0,0 @@
1
- # coding: utf-8
2
- module OrderQuery
3
- # Build where clause for searching around a record in an order space
4
- class WhereBuilder
5
- # @return [ActiveRecord::Base]
6
- attr_reader :record
7
- # @return [OrderQuery::OrderSpace]
8
- attr_reader :order
9
-
10
- # @param [ActiveRecord::Base] record
11
- # @param [OrderQuery::OrderSpace] order_space
12
- def initialize(record, order_space)
13
- @order = order_space
14
- @record = record
15
- end
16
-
17
- # @param [:before or :after] mode
18
- # @return [query, parameters] conditions that exclude all elements not before / after the current one
19
- def build_query(mode)
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
28
- # Wrap top level OR clause for performance, see https://github.com/glebm/order_query/issues/3
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)
32
- else
33
- query
34
- end
35
- end
36
-
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))
57
- else
58
- disjunctive
59
- end
60
- end
61
-
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
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
84
- else
85
- where_ray cond, value, mode, strict
86
- end
87
- end
88
-
89
-
90
- def where_in(cond, values)
91
- case values.length
92
- when 0
93
- WHERE_NONE
94
- when 1
95
- where_eq cond, values[0]
96
- else
97
- ["#{cond.col_name_sql} IN (?)".freeze, [values]]
98
- end
99
- end
100
-
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]]
110
- end
111
-
112
- WHERE_IDENTITY = [''.freeze, [].freeze].freeze
113
- WHERE_NONE = ['∅'.freeze, [].freeze].freeze
114
-
115
- def attr_value(cond)
116
- record.send cond.name
117
- end
118
-
119
- class << self
120
- attr_accessor :wrap_top_level_or
121
- end
122
- self.wrap_top_level_or = true
123
- end
124
- end