nobrainer 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +2 -2
  3. data/lib/no_brainer/criteria.rb +3 -2
  4. data/lib/no_brainer/criteria/after_find.rb +1 -1
  5. data/lib/no_brainer/criteria/aggregate.rb +25 -0
  6. data/lib/no_brainer/criteria/core.rb +15 -1
  7. data/lib/no_brainer/criteria/count.rb +1 -1
  8. data/lib/no_brainer/criteria/delete.rb +2 -2
  9. data/lib/no_brainer/criteria/index.rb +37 -0
  10. data/lib/no_brainer/criteria/order_by.rb +53 -13
  11. data/lib/no_brainer/criteria/pluck.rb +81 -0
  12. data/lib/no_brainer/criteria/raw.rb +12 -4
  13. data/lib/no_brainer/criteria/scope.rb +11 -6
  14. data/lib/no_brainer/criteria/update.rb +12 -4
  15. data/lib/no_brainer/criteria/where.rb +133 -64
  16. data/lib/no_brainer/document.rb +4 -3
  17. data/lib/no_brainer/document/aliases.rb +55 -0
  18. data/lib/no_brainer/document/association.rb +1 -1
  19. data/lib/no_brainer/document/association/belongs_to.rb +2 -2
  20. data/lib/no_brainer/document/attributes.rb +26 -8
  21. data/lib/no_brainer/document/criteria.rb +7 -4
  22. data/lib/no_brainer/document/dirty.rb +21 -4
  23. data/lib/no_brainer/document/dynamic_attributes.rb +4 -1
  24. data/lib/no_brainer/document/index.rb +47 -12
  25. data/lib/no_brainer/document/lazy_fetch.rb +72 -0
  26. data/lib/no_brainer/document/missing_attributes.rb +73 -0
  27. data/lib/no_brainer/document/persistance.rb +57 -8
  28. data/lib/no_brainer/document/polymorphic.rb +14 -8
  29. data/lib/no_brainer/document/types.rb +5 -2
  30. data/lib/no_brainer/document/types/binary.rb +23 -0
  31. data/lib/no_brainer/document/uniqueness.rb +1 -1
  32. data/lib/no_brainer/error.rb +16 -0
  33. data/lib/no_brainer/index_manager.rb +1 -0
  34. data/lib/no_brainer/query_runner.rb +3 -1
  35. data/lib/no_brainer/query_runner/connection_lock.rb +7 -0
  36. data/lib/no_brainer/query_runner/database_on_demand.rb +6 -3
  37. data/lib/no_brainer/query_runner/logger.rb +22 -6
  38. data/lib/no_brainer/query_runner/missing_index.rb +5 -3
  39. data/lib/no_brainer/query_runner/table_on_demand.rb +6 -3
  40. data/lib/no_brainer/railtie.rb +1 -1
  41. metadata +12 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e97dd4f563340a2c8551ffefb21f22fc628954b2
4
- data.tar.gz: 1fca054e8f966be9e7518f8245c39987d40068c7
3
+ metadata.gz: fe36d41b56c9780a4d16f3085d31de0361d34cae
4
+ data.tar.gz: 9f89b0da1f34d65c8861345307adc79f93a5593d
5
5
  SHA512:
6
- metadata.gz: 1cbfebfa6ac2922e733d157522eda4e6c9edf9de9e51601f6892ac2ac2814593ed20807c68441a2ff7391398cf37896eeb739eaf2cdafa0da8e9fe91bb02fce2
7
- data.tar.gz: ace1719ae85c7593145e81818fb1b93f715ad65d4634130c664b0fdd134c637aba0395066163b9fd3aee91f7ec0b27a53338e857ec2c089a4b425e02f2eba00b
6
+ metadata.gz: 43e0cf6f767028a46068635713cb9c1d639a8e03de6503493a6af05179977dfe1ae01b5c6a49d93ec0f2e4012a24b0386b6f66d31c7dd274ec05f5f69b252ba3
7
+ data.tar.gz: e728763fc6095d493db2ed93a9141f72d7cfc927f35300993f0ca6272996a5bac120f6f9cbfc9a985d80329a78da88ec5d827d5af31ccce93c833704b3a17f0b
@@ -15,11 +15,11 @@ module NoBrainer::Autoload
15
15
 
16
16
  def autoload_and_include(*constants)
17
17
  constants.each { |constant| autoload constant }
18
- constants.each { |constant| include const_get constant }
18
+ constants.each { |constant| include const_get(constant) }
19
19
  end
20
20
 
21
21
  def eager_autoload_and_include(*constants)
22
22
  eager_autoload(*constants)
23
- constants.each { |constant| include const_get constant }
23
+ constants.each { |constant| include const_get(constant) }
24
24
  end
25
25
  end
@@ -2,6 +2,7 @@ require 'rethinkdb'
2
2
 
3
3
  class NoBrainer::Criteria
4
4
  extend NoBrainer::Autoload
5
- autoload_and_include :Core, :Scope, :Raw, :AfterFind, :Where, :OrderBy, :Limit,
6
- :Count, :Delete, :Enumerable, :First, :Preload, :Update, :Cache
5
+ autoload_and_include :Core, :Raw, :AfterFind, :Where, :OrderBy, :Limit,
6
+ :Pluck, :Count, :Delete, :Enumerable, :First, :Aggregate,
7
+ :Preload, :Update, :Cache, :Index, :Scope
7
8
  end
@@ -15,7 +15,7 @@ module NoBrainer::Criteria::AfterFind
15
15
  self
16
16
  end
17
17
 
18
- def instantiate_doc(attrs)
18
+ def _instantiate_doc(attrs)
19
19
  super.tap do |doc|
20
20
  self._after_find.to_a.each { |block| block.call(doc) }
21
21
  doc.run_callbacks(:find) if doc.is_a?(NoBrainer::Document)
@@ -0,0 +1,25 @@
1
+ module NoBrainer::Criteria::Aggregate
2
+ extend ActiveSupport::Concern
3
+
4
+ def min(*a, &b)
5
+ instantiate_doc NoBrainer.run { aggregate_rql(:min, *a, &b) }
6
+ end
7
+
8
+ def max(*a, &b)
9
+ instantiate_doc NoBrainer.run { aggregate_rql(:max, *a, &b) }
10
+ end
11
+
12
+ def sum(*a, &b)
13
+ NoBrainer.run { aggregate_rql(:sum, *a, &b) }
14
+ end
15
+
16
+ def avg(*a, &b)
17
+ NoBrainer.run { aggregate_rql(:avg, *a, &b) }
18
+ end
19
+
20
+ private
21
+
22
+ def aggregate_rql(type, *a, &b)
23
+ without_ordering.without_plucking.to_rql.__send__(type, *klass.with_fields_aliased(a), &b)
24
+ end
25
+ end
@@ -12,7 +12,7 @@ module NoBrainer::Criteria::Core
12
12
  end
13
13
 
14
14
  def to_rql
15
- with_default_scope_applied.__send__(:compile_rql_pass2)
15
+ finalized_criteria.__send__(:compile_rql_pass2)
16
16
  end
17
17
 
18
18
  def inspect
@@ -56,4 +56,18 @@ module NoBrainer::Criteria::Core
56
56
  # This method is overriden by other modules.
57
57
  compile_rql_pass1
58
58
  end
59
+
60
+ def finalized?
61
+ !!init_options[:finalized]
62
+ end
63
+
64
+ def finalized_criteria
65
+ @finalized_criteria ||= finalized? ? self : self.class._finalize_criteria(self)
66
+ end
67
+
68
+ module ClassMethods
69
+ def _finalize_criteria(base)
70
+ base.merge(base.class.new(:finalized => true))
71
+ end
72
+ end
59
73
  end
@@ -2,7 +2,7 @@ module NoBrainer::Criteria::Count
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def count
5
- run(without_ordering.to_rql.count)
5
+ run(without_ordering.without_plucking.to_rql.count)
6
6
  end
7
7
 
8
8
  def empty?
@@ -2,10 +2,10 @@ module NoBrainer::Criteria::Delete
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def delete_all
5
- run(without_ordering.to_rql.delete)
5
+ run(without_ordering.without_plucking.to_rql.delete)
6
6
  end
7
7
 
8
8
  def destroy_all
9
- without_ordering.to_a.each { |doc| doc.destroy }
9
+ to_a.each { |doc| doc.destroy }
10
10
  end
11
11
  end
@@ -0,0 +1,37 @@
1
+ module NoBrainer::Criteria::Index
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :with_index_name }
5
+
6
+ def with_index(index_name=true)
7
+ chain { |criteria| criteria.with_index_name = index_name }
8
+ end
9
+
10
+ def without_index
11
+ with_index(false)
12
+ end
13
+
14
+ def without_index?
15
+ finalized_criteria.with_index_name == false
16
+ end
17
+
18
+ def used_index
19
+ # only one of them will be active.
20
+ where_index_name || order_by_index_name
21
+ end
22
+
23
+ def merge!(criteria, options={})
24
+ super
25
+ self.with_index_name = criteria.with_index_name unless criteria.with_index_name.nil?
26
+ self
27
+ end
28
+
29
+ def compile_rql_pass2
30
+ super.tap do
31
+ if with_index_name && (!used_index || order_by_index_name.to_s == klass.pk_name.to_s)
32
+ # The implicit ordering on the indexed pk does not count.
33
+ raise NoBrainer::Error::CannotUseIndex.new(with_index_name)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -16,8 +16,7 @@ module NoBrainer::Criteria::OrderBy
16
16
  bad_rule = rule.values.reject { |v| v.in? [:asc, :desc] }.first
17
17
  raise_bad_rule(bad_rule) if bad_rule
18
18
  rule
19
- when Symbol then { rule => :asc }
20
- when Proc then { rule => :asc }
19
+ when String, Symbol, Proc then { rule => :asc }
21
20
  else raise_bad_rule(rule)
22
21
  end
23
22
  end.reduce({}, :merge)
@@ -52,6 +51,14 @@ module NoBrainer::Criteria::OrderBy
52
51
  end
53
52
  end
54
53
 
54
+ def order_by_indexed?
55
+ !!order_by_index_name
56
+ end
57
+
58
+ def order_by_index_name
59
+ order_by_index_finder.index_name
60
+ end
61
+
55
62
  private
56
63
 
57
64
  def effective_order
@@ -66,6 +73,41 @@ module NoBrainer::Criteria::OrderBy
66
73
  self.ordering_mode != :disabled
67
74
  end
68
75
 
76
+ class IndexFinder < Struct.new(:criteria, :index_name, :rql_proc)
77
+ def could_find_index?
78
+ !!self.index_name
79
+ end
80
+
81
+ def first_key
82
+ @first_key ||= criteria.__send__(:effective_order).first.try(:[], 0)
83
+ end
84
+
85
+ def first_key_indexable?
86
+ (first_key.is_a?(Symbol) || first_key.is_a?(String)) && criteria.klass.has_index?(first_key)
87
+ end
88
+
89
+ def find_index
90
+ return if criteria.without_index?
91
+ return unless first_key_indexable?
92
+
93
+ if criteria.with_index_name && criteria.with_index_name != true
94
+ return unless first_key.to_s == criteria.with_index_name.to_s
95
+ end
96
+
97
+ # We need make sure that the where index finder has been invoked, it has priority.
98
+ # If it doesn't find anything, we are free to go with our indexes.
99
+ if !criteria.where_indexed? || (criteria.where_index_type == :between &&
100
+ first_key.to_s == criteria.where_index_name.to_s)
101
+ self.index_name = first_key
102
+ end
103
+ end
104
+ end
105
+
106
+ def order_by_index_finder
107
+ return finalized_criteria.__send__(:order_by_index_finder) unless finalized?
108
+ @order_by_index_finder ||= IndexFinder.new(self).tap { |index_finder| index_finder.find_index }
109
+ end
110
+
69
111
  def compile_rql_pass1
70
112
  rql = super
71
113
  return rql unless should_order?
@@ -73,6 +115,12 @@ module NoBrainer::Criteria::OrderBy
73
115
  return rql if _effective_order.empty?
74
116
 
75
117
  rql_rules = _effective_order.map do |k,v|
118
+ if order_by_index_finder.index_name == k
119
+ k = klass.lookup_index_alias(k)
120
+ else
121
+ k = klass.lookup_field_alias(k)
122
+ end
123
+
76
124
  case v
77
125
  when :asc then reverse_order? ? RethinkDB::RQL.new.desc(k) : RethinkDB::RQL.new.asc(k)
78
126
  when :desc then reverse_order? ? RethinkDB::RQL.new.asc(k) : RethinkDB::RQL.new.desc(k)
@@ -82,19 +130,11 @@ module NoBrainer::Criteria::OrderBy
82
130
  # We can only apply an index order_by on a table() term.
83
131
  # We are going to try to go so and if we cannot, we'll simply apply
84
132
  # the ordering in pass2, which will happen after a potential filter().
85
-
86
- NoBrainer::RQL.is_table?(rql)
87
- if NoBrainer::RQL.is_table?(rql) && !without_index?
88
- options = {}
89
- first_key = _effective_order.first[0]
90
- if (first_key.is_a?(Symbol) || first_key.is_a?(String)) && klass.has_index?(first_key)
91
- options[:index] = rql_rules.shift
92
- end
93
-
133
+ if order_by_index_finder.could_find_index?
134
+ options = { :index => rql_rules.shift }
94
135
  rql = rql.order_by(*rql_rules, options)
95
136
  else
96
- # Stashing @rql_rules for pass2, which is a pretty gross hack.
97
- # We should really use more of a middleware pattern to build the RQL.
137
+ # Stashing @rql_rules for pass2
98
138
  @rql_rules_pass2 = rql_rules
99
139
  end
100
140
 
@@ -0,0 +1,81 @@
1
+ module NoBrainer::Criteria::Pluck
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :missing_attributes }
5
+
6
+ def pluck(*attrs)
7
+ _missing_attributes_criteria(:pluck, attrs)
8
+ end
9
+
10
+ def without(*attrs)
11
+ _missing_attributes_criteria(:without, attrs)
12
+ end
13
+
14
+ def lazy_fetch(*attrs)
15
+ _missing_attributes_criteria(:lazy_fetch, attrs)
16
+ end
17
+
18
+ def without_plucking
19
+ chain { |criteria| criteria.missing_attributes = :remove }
20
+ end
21
+
22
+ def merge!(criteria, options={})
23
+ return super unless criteria.missing_attributes
24
+
25
+ if criteria.missing_attributes == :remove
26
+ self.missing_attributes = nil
27
+ else
28
+ self.missing_attributes ||= {}
29
+ criteria.missing_attributes.each do |type, attrs|
30
+ old_attrs = self.missing_attributes[type] || {}.with_indifferent_access
31
+ new_attrs = old_attrs.deep_merge(attrs)
32
+ self.missing_attributes[type] = new_attrs
33
+ end
34
+ end
35
+
36
+ self
37
+ end
38
+
39
+ private
40
+
41
+ def _missing_attributes_criteria(type, args)
42
+ raise ArgumentError if args.size.zero?
43
+ args = [Hash[args.flatten.map { |k| [k, true] }]] unless args.size == 1 && args.first.is_a?(Hash)
44
+ chain { |criteria| criteria.missing_attributes = {type => args.first} }
45
+
46
+ end
47
+
48
+ def effective_missing_attributes
49
+ return nil if self.missing_attributes.nil?
50
+ @effective_missing_attributes ||= begin
51
+ # pluck gets priority
52
+
53
+ missing_attributes = Hash[self.missing_attributes.map do |type, attrs|
54
+ attrs = attrs.select { |k,v| v } # TODO recursive
55
+ attrs.empty? ? nil : [type, attrs]
56
+ end.compact]
57
+
58
+ if missing_attributes[:pluck]
59
+ { :pluck => missing_attributes[:pluck] }
60
+ else
61
+ attrs = [missing_attributes[:without], missing_attributes[:lazy_fetch]].compact.reduce(&:merge)
62
+ { :without => attrs } if attrs.present?
63
+ end
64
+ end
65
+ end
66
+
67
+ def _instantiate_model(attrs, options={})
68
+ return super if missing_attributes.nil?
69
+ super(attrs, options.merge(:missing_attributes => effective_missing_attributes,
70
+ :lazy_fetch => missing_attributes[:lazy_fetch]))
71
+ end
72
+
73
+ def compile_rql_pass2
74
+ rql = super
75
+ if effective_missing_attributes
76
+ type, attrs = effective_missing_attributes.first
77
+ rql = rql.__send__(type, klass.with_fields_aliased(attrs))
78
+ end
79
+ rql
80
+ end
81
+ end
@@ -13,13 +13,21 @@ module NoBrainer::Criteria::Raw
13
13
  self
14
14
  end
15
15
 
16
- private
17
-
18
16
  def raw?
19
- !!_raw
17
+ !!finalized_criteria._raw
20
18
  end
21
19
 
20
+ private
21
+
22
22
  def instantiate_doc(attrs)
23
- raw? ? attrs : klass.new_from_db(attrs)
23
+ finalized_criteria._instantiate_doc(attrs)
24
+ end
25
+
26
+ def _instantiate_doc(attrs)
27
+ raw? ? attrs : _instantiate_model(attrs)
28
+ end
29
+
30
+ def _instantiate_model(attrs, options={})
31
+ klass.new_from_db(attrs, options)
24
32
  end
25
33
  end
@@ -34,12 +34,17 @@ module NoBrainer::Criteria::Scope
34
34
  klass.default_scope_proc && use_default_scope != false
35
35
  end
36
36
 
37
- def with_default_scope_applied
38
- if should_apply_default_scope?
39
- # XXX If default_scope.class != self.class, oops
40
- klass.default_scope_proc.call.merge(self).unscoped
41
- else
42
- self
37
+ def _apply_default_scope
38
+ return unless should_apply_default_scope?
39
+ criteria = klass.default_scope_proc.call
40
+ raise "Mixing model issue. Contact developer." if [criteria.klass, self.klass].compact.uniq.size == 2
41
+ criteria.merge(self)
42
+ end
43
+
44
+ module ClassMethods
45
+ def _finalize_criteria(base)
46
+ criteria = super
47
+ criteria.__send__(:_apply_default_scope) || criteria
43
48
  end
44
49
  end
45
50
  end
@@ -1,11 +1,19 @@
1
1
  module NoBrainer::Criteria::Update
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def update_all(*args, &block)
5
- run(without_ordering.to_rql.update(*args, &block))
4
+ def update_all(*a, &b)
5
+ prepare_args_for_update!(a)
6
+ run(without_ordering.without_plucking.to_rql.update(*a, &b))
6
7
  end
7
8
 
8
- def replace_all(*args, &block)
9
- run(without_ordering.to_rql.replace(*args, &block))
9
+ def replace_all(*a, &b)
10
+ prepare_args_for_update!(a)
11
+ run(without_ordering.without_plucking.to_rql.replace(*a, &b))
12
+ end
13
+
14
+ private
15
+
16
+ def prepare_args_for_update!(a)
17
+ a[0] = klass.persistable_attributes(a[0]) if !a.empty? && a.first.is_a?(Hash)
10
18
  end
11
19
  end
@@ -5,45 +5,60 @@ module NoBrainer::Criteria::Where
5
5
 
6
6
  def initialize(options={})
7
7
  super
8
- self.where_ast = MultiOperator.new(:and, [])
9
8
  end
10
9
 
11
10
  def where(*args, &block)
12
11
  chain { |criteria| criteria.where_ast = parse_clause([*args, block].compact) }
13
12
  end
14
13
 
15
- def with_index(index_name)
16
- chain { |criteria| criteria.with_index_name = index_name }
14
+ def merge!(criteria, options={})
15
+ super
16
+
17
+ if criteria.where_ast
18
+ if self.where_ast
19
+ self.where_ast = MultiOperator.new(:and, [self.where_ast, criteria.where_ast])
20
+ else
21
+ self.where_ast = criteria.where_ast
22
+ end
23
+ self.where_ast = self.where_ast.simplify
24
+ raise unless criteria.where_ast.is_a?(MultiOperator)
25
+ end
26
+
27
+ self.with_index_name = criteria.with_index_name unless criteria.with_index_name.nil?
28
+ self
17
29
  end
18
30
 
19
- def without_index
20
- with_index(false)
31
+ def where_present?
32
+ finalized_criteria.where_ast.try(:clauses).present?
21
33
  end
22
34
 
23
- def used_index
24
- index_finder.index_name
35
+ def where_indexed?
36
+ !!where_index_name
25
37
  end
26
38
 
27
- def indexed?
28
- index_finder.could_find_index?
39
+ def where_index_name
40
+ where_index_finder.index_name
29
41
  end
30
42
 
31
- def merge!(criteria, options={})
32
- super
33
- clauses = self.where_ast.clauses + criteria.where_ast.clauses
34
- self.where_ast = MultiOperator.new(:and, clauses).simplify
35
- self.with_index_name = criteria.with_index_name unless criteria.with_index_name.nil?
36
- self
43
+ def where_index_type
44
+ where_index_finder.index_type
37
45
  end
38
46
 
39
47
  private
40
48
 
41
49
  class MultiOperator < Struct.new(:op, :clauses)
42
50
  def simplify
43
- same_op_clauses, other_clauses = self.clauses.map(&:simplify)
44
- .partition { |v| v.is_a?(MultiOperator) && self.op == v.op }
51
+ clauses = self.clauses.map(&:simplify)
52
+ if self.clauses.size == 1 && self.clauses.first.is_a?(self.class)
53
+ return clauses.first
54
+ end
55
+
56
+ same_op_clauses, other_clauses = clauses.partition do |v|
57
+ v.is_a?(self.class) && (v.clauses.size == 1 || v.op == self.op)
58
+ end
45
59
  simplified_clauses = other_clauses + same_op_clauses.map(&:clauses).flatten(1)
46
- MultiOperator.new(op, simplified_clauses.uniq)
60
+ simplified_clauses = BinaryOperator.simplify_clauses(op, simplified_clauses.uniq)
61
+ self.class.new(op, simplified_clauses)
47
62
  end
48
63
 
49
64
  def to_rql(doc)
@@ -54,22 +69,48 @@ module NoBrainer::Criteria::Where
54
69
  end
55
70
  end
56
71
 
57
- class BinaryOperator < Struct.new(:key, :op, :value, :criteria, :casted_values)
72
+ class BinaryOperator < Struct.new(:key, :op, :value, :model, :casted_values)
73
+ def self.get_candidate_clauses(clauses, *types)
74
+ clauses.select { |c| c.is_a?(self) && types.include?(c.op) }
75
+ end
76
+
77
+ def self.simplify_clauses(op, ast_clauses)
78
+ # This code assumes that simplfy() has already been called on all clauses.
79
+ if op == :or
80
+ eq_clauses = get_candidate_clauses(ast_clauses, :in, :eq)
81
+ new_clauses = eq_clauses.group_by(&:key).map do |key, clauses|
82
+ case clauses.size
83
+ when 1 then clauses.first
84
+ else
85
+ values = clauses.map { |c| c.op == :in ? c.value : [c.value] }.flatten(1).uniq
86
+ BinaryOperator.new(key, :in, values, clauses.first.model, true)
87
+ end
88
+ end
89
+
90
+ if new_clauses.size != eq_clauses.size
91
+ ast_clauses = ast_clauses - eq_clauses + new_clauses
92
+ end
93
+ end
94
+
95
+ ast_clauses
96
+ end
97
+
58
98
  def simplify
59
99
  key = cast_key(self.key)
60
100
  case op
61
101
  when :in then
62
102
  case value
63
- when Range then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), criteria, true)
64
- when Array then BinaryOperator.new(key, :in, value.map(&method(:cast_value)).uniq, criteria, true)
103
+ when Range then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), model, true)
104
+ when Array then BinaryOperator.new(key, :in, value.map(&method(:cast_value)).uniq, model, true)
65
105
  else raise ArgumentError.new ":in takes an array/range, not #{value}"
66
106
  end
67
- when :between then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), criteria, true)
68
- else BinaryOperator.new(key, op, cast_value(value), criteria, true)
107
+ when :between then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), model, true)
108
+ else BinaryOperator.new(key, op, cast_value(value), model, true)
69
109
  end
70
110
  end
71
111
 
72
112
  def to_rql(doc)
113
+ key = model.lookup_field_alias(self.key)
73
114
  case op
74
115
  when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
75
116
  when :between then (doc[key] >= value.min) & (doc[key] <= value.max)
@@ -84,7 +125,7 @@ module NoBrainer::Criteria::Where
84
125
  # FIXME This leaks memory with dynamic attributes. The internals of type
85
126
  # checking will convert the key to a symbol, and Ruby does not garbage
86
127
  # collect symbols.
87
- @association ||= [criteria.klass.association_metadata[key.to_sym]]
128
+ @association ||= [model.association_metadata[key.to_sym]]
88
129
  @association.first
89
130
  end
90
131
 
@@ -98,7 +139,7 @@ module NoBrainer::Criteria::Where
98
139
  raise NoBrainer::Error::InvalidType.new(opts) unless value.is_a?(target_klass)
99
140
  value.pk_value
100
141
  else
101
- criteria.klass.cast_user_to_db_for(key, value)
142
+ model.cast_user_to_db_for(key, value)
102
143
  end
103
144
  end
104
145
 
@@ -106,10 +147,8 @@ module NoBrainer::Criteria::Where
106
147
  return key if casted_values
107
148
 
108
149
  case association
109
- when NoBrainer::Document::Association::BelongsTo::Metadata
110
- association.foreign_key
111
- else
112
- key
150
+ when NoBrainer::Document::Association::BelongsTo::Metadata then association.foreign_key
151
+ else key
113
152
  end
114
153
  end
115
154
  end
@@ -161,7 +200,7 @@ module NoBrainer::Criteria::Where
161
200
  when :nin then parse_clause(:not => { key.symbol.in => value })
162
201
  when :ne then parse_clause(:not => { key.symbol.eq => value })
163
202
  when :eq then parse_clause_stub_eq(key.symbol, value)
164
- else BinaryOperator.new(key.symbol, key.modifier, value, self)
203
+ else BinaryOperator.new(key.symbol, key.modifier, value, self.klass)
165
204
  end
166
205
  else raise "Invalid key: #{key}"
167
206
  end
@@ -169,37 +208,40 @@ module NoBrainer::Criteria::Where
169
208
 
170
209
  def parse_clause_stub_eq(key, value)
171
210
  case value
172
- when Range then BinaryOperator.new(key, :between, value, self)
173
- when Regexp then BinaryOperator.new(key, :match, value.inspect[1..-2], self)
174
- else BinaryOperator.new(key, :eq, value, self)
211
+ when Range then BinaryOperator.new(key, :between, value, self.klass)
212
+ when Regexp then BinaryOperator.new(key, :match, value.inspect[1..-2], self.klass)
213
+ else BinaryOperator.new(key, :eq, value, self.klass)
175
214
  end
176
215
  end
177
216
 
178
- def without_index?
179
- self.with_index_name == false
180
- end
181
-
182
- class IndexFinder < Struct.new(:criteria, :index_name, :rql_proc, :ast)
217
+ class IndexFinder < Struct.new(:criteria, :ast, :index_name, :index_type, :rql_proc)
183
218
  def initialize(*args)
184
219
  super
185
- find_index
186
220
  end
187
221
 
188
222
  def could_find_index?
189
223
  !!self.index_name
190
224
  end
191
225
 
192
- private
193
-
194
226
  def get_candidate_clauses(*types)
195
- criteria.where_ast.clauses.select { |c| c.is_a?(BinaryOperator) && types.include?(c.op) }
227
+ BinaryOperator.get_candidate_clauses(ast.clauses, *types)
196
228
  end
197
229
 
198
230
  def get_usable_indexes(*types)
199
- indexes = criteria.klass.indexes
200
- indexes = indexes.select { |k,v| types.include?(v[:kind]) } if types.present?
201
- indexes = indexes.select { |k,v| k == criteria.with_index_name.to_sym } if criteria.with_index_name
202
- indexes
231
+ @usable_indexes = {}
232
+ @usable_indexes[types] ||= begin
233
+ indexes = criteria.klass.indexes
234
+ indexes = indexes.select { |k,v| types.include?(v[:kind]) } if types.present?
235
+ if criteria.with_index_name && criteria.with_index_name != true
236
+ indexes = indexes.select { |k,v| k == criteria.with_index_name.to_sym }
237
+ end
238
+ indexes
239
+ end
240
+ end
241
+
242
+ def remove_from_ast(clauses)
243
+ new_ast = MultiOperator.new(ast.op, ast.clauses - clauses)
244
+ return new_ast if new_ast.clauses.present?
203
245
  end
204
246
 
205
247
  def find_index_canonical
@@ -208,12 +250,14 @@ module NoBrainer::Criteria::Where
208
250
 
209
251
  if index_name = (get_usable_indexes.keys & clauses.keys).first
210
252
  clause = clauses[index_name]
253
+ aliased_index = criteria.klass.indexes[index_name][:as]
211
254
  self.index_name = index_name
212
- self.ast = MultiOperator.new(:and, criteria.where_ast.clauses - [clause])
255
+ self.ast = remove_from_ast([clause])
256
+ self.index_type = clause.op == :between ? :between : :get_all
213
257
  self.rql_proc = case clause.op
214
- when :eq then ->(rql){ rql.get_all(clause.value, :index => index_name) }
215
- when :in then ->(rql){ rql.get_all(*clause.value, :index => index_name) }
216
- when :between then ->(rql){ rql.between(clause.value.min, clause.value.max, :index => index_name,
258
+ when :eq then ->(rql){ rql.get_all(clause.value, :index => aliased_index) }
259
+ when :in then ->(rql){ rql.get_all(*clause.value, :index => aliased_index) }
260
+ when :between then ->(rql){ rql.between(clause.value.min, clause.value.max, :index => aliased_index,
217
261
  :left_bound => :closed, :right_bound => :closed) }
218
262
  end
219
263
  end
@@ -230,9 +274,11 @@ module NoBrainer::Criteria::Where
230
274
 
231
275
  if index_name
232
276
  indexed_clauses = index_values.map { |field| clauses[field] }
277
+ aliased_index = criteria.klass.indexes[index_name][:as]
233
278
  self.index_name = index_name
234
- self.ast = MultiOperator.new(:and, criteria.where_ast.clauses - indexed_clauses)
235
- self.rql_proc = ->(rql){ rql.get_all(indexed_clauses.map { |c| c.value }, :index => index_name) }
279
+ self.ast = remove_from_ast(indexed_clauses)
280
+ self.index_type = :get_all
281
+ self.rql_proc = ->(rql){ rql.get_all(indexed_clauses.map { |c| c.value }, :index => aliased_index) }
236
282
  end
237
283
  end
238
284
 
@@ -245,40 +291,63 @@ module NoBrainer::Criteria::Where
245
291
  left_bound = op_clauses[:gt] || op_clauses[:ge]
246
292
  right_bound = op_clauses[:lt] || op_clauses[:le]
247
293
 
294
+ aliased_index = criteria.klass.indexes[index_name][:as]
248
295
  self.index_name = index_name
249
- self.ast = MultiOperator.new(:and, criteria.where_ast.clauses - [left_bound, right_bound].compact)
296
+ self.ast = remove_from_ast([left_bound, right_bound].compact)
250
297
 
251
- options = {:index => index_name}
298
+ options = {}
299
+ options[:index] = aliased_index
252
300
  options[:left_bound] = {:gt => :open, :ge => :closed}[left_bound.op] if left_bound
253
301
  options[:right_bound] = {:lt => :open, :le => :closed}[right_bound.op] if right_bound
302
+ self.index_type = :between
254
303
  self.rql_proc = ->(rql){ rql.between(left_bound.try(:value), right_bound.try(:value), options) }
255
304
  end
256
305
  end
257
306
 
307
+ def find_union_index
308
+ indexes = []
309
+ index_finder = self
310
+
311
+ loop do
312
+ index_finder = index_finder.dup
313
+ break unless index_finder.find_index_canonical
314
+ # TODO To use a compound index, we'd have to add all permutations in the query
315
+ indexes << index_finder
316
+ break unless index_finder.ast
317
+ end
318
+
319
+ if indexes.present? && !index_finder.ast
320
+ self.ast = nil
321
+ self.index_name = indexes.map(&:index_name)
322
+ self.index_type = indexes.map(&:index_type)
323
+ self.rql_proc = ->(rql){ indexes.map { |index| index.rql_proc.call(rql) }.reduce { |a,b| a.union(b) } }
324
+ end
325
+ end
326
+
258
327
  def find_index
259
- return if criteria.__send__(:without_index?)
260
- find_index_canonical || find_index_compound || find_index_hidden_between
261
- if criteria.with_index_name && !could_find_index?
262
- raise NoBrainer::Error::CannotUseIndex.new("Cannot use index #{criteria.with_index_name}")
328
+ return if ast.nil? || criteria.without_index?
329
+ case ast.op
330
+ when :and then find_index_compound || find_index_canonical || find_index_hidden_between
331
+ when :or then find_union_index
263
332
  end
264
333
  end
265
334
  end
266
335
 
267
- def index_finder
268
- return with_default_scope_applied.__send__(:index_finder) if should_apply_default_scope?
269
- @index_finder ||= IndexFinder.new(self)
336
+ def where_index_finder
337
+ return finalized_criteria.__send__(:where_index_finder) unless finalized?
338
+ @where_index_finder ||= IndexFinder.new(self, where_ast).tap { |index_finder| index_finder.find_index }
270
339
  end
271
340
 
272
341
  def compile_rql_pass1
273
342
  rql = super
274
- rql = index_finder.rql_proc.call(rql) if index_finder.could_find_index?
343
+ rql = where_index_finder.rql_proc.call(rql) if where_index_finder.could_find_index?
275
344
  rql
276
345
  end
277
346
 
278
347
  def compile_rql_pass2
279
348
  rql = super
280
- ast = index_finder.could_find_index? ? index_finder.ast : self.where_ast
281
- rql = rql.filter { |doc| ast.to_rql(doc) } if ast.clauses.present?
349
+ ast = where_index_finder.could_find_index? ? where_index_finder.ast : self.where_ast
350
+ rql = rql.filter { |doc| ast.to_rql(doc) } if ast.try(:clauses).present?
282
351
  rql
283
352
  end
284
353
  end