order_query 0.1.3 → 0.2.0

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