nobrainer 0.15.0 → 0.16.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.
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