nobrainer 0.18.0 → 0.19.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +35 -37
  3. data/lib/no_brainer/connection.rb +1 -1
  4. data/lib/no_brainer/criteria/after_find.rb +3 -11
  5. data/lib/no_brainer/criteria/cache.rb +7 -7
  6. data/lib/no_brainer/criteria/core.rb +49 -10
  7. data/lib/no_brainer/criteria/delete.rb +1 -1
  8. data/lib/no_brainer/criteria/extend.rb +4 -16
  9. data/lib/no_brainer/criteria/index.rb +7 -13
  10. data/lib/no_brainer/criteria/limit.rb +5 -12
  11. data/lib/no_brainer/criteria/order_by.rb +19 -35
  12. data/lib/no_brainer/criteria/pluck.rb +16 -22
  13. data/lib/no_brainer/criteria/preload.rb +9 -15
  14. data/lib/no_brainer/criteria/raw.rb +4 -10
  15. data/lib/no_brainer/criteria/scope.rb +4 -10
  16. data/lib/no_brainer/criteria/where.rb +172 -130
  17. data/lib/no_brainer/document/aliases.rb +10 -2
  18. data/lib/no_brainer/document/association/belongs_to.rb +3 -3
  19. data/lib/no_brainer/document/association/core.rb +1 -1
  20. data/lib/no_brainer/document/association/eager_loader.rb +4 -3
  21. data/lib/no_brainer/document/association/has_many.rb +2 -2
  22. data/lib/no_brainer/document/atomic_ops.rb +29 -30
  23. data/lib/no_brainer/document/attributes.rb +15 -19
  24. data/lib/no_brainer/document/callbacks.rb +1 -1
  25. data/lib/no_brainer/document/id.rb +7 -3
  26. data/lib/no_brainer/document/index.rb +20 -10
  27. data/lib/no_brainer/document/persistance.rb +11 -10
  28. data/lib/no_brainer/document/timestamps.rb +4 -2
  29. data/lib/no_brainer/document/types/binary.rb +0 -1
  30. data/lib/no_brainer/document/types/boolean.rb +0 -1
  31. data/lib/no_brainer/document/types.rb +0 -9
  32. data/lib/no_brainer/document/uniqueness.rb +0 -1
  33. data/lib/no_brainer/document/validation.rb +5 -5
  34. data/lib/no_brainer/error.rb +1 -0
  35. data/lib/no_brainer/query_runner/connection_lock.rb +1 -1
  36. data/lib/no_brainer/query_runner/reconnect.rb +9 -11
  37. data/lib/no_brainer/query_runner/table_on_demand.rb +1 -1
  38. data/lib/no_brainer/railtie/database.rake +2 -2
  39. data/lib/no_brainer/rql.rb +1 -1
  40. data/lib/nobrainer.rb +1 -6
  41. data/lib/rails/generators/nobrainer.rb +1 -1
  42. metadata +18 -5
  43. data/lib/no_brainer/decorated_symbol.rb +0 -17
@@ -1,20 +1,14 @@
1
1
  module NoBrainer::Criteria::Raw
2
2
  extend ActiveSupport::Concern
3
3
 
4
- included { attr_accessor :_raw }
4
+ included { criteria_option :raw, :merge_with => :set_scalar }
5
5
 
6
- def raw
7
- chain { |criteria| criteria._raw = true }
8
- end
9
-
10
- def merge!(criteria, options={})
11
- super
12
- self._raw = criteria._raw unless criteria._raw.nil?
13
- self
6
+ def raw(value = true)
7
+ chain(:raw => value)
14
8
  end
15
9
 
16
10
  def raw?
17
- !!finalized_criteria._raw
11
+ !!finalized_criteria.options[:raw]
18
12
  end
19
13
 
20
14
  private
@@ -1,20 +1,14 @@
1
1
  module NoBrainer::Criteria::Scope
2
2
  extend ActiveSupport::Concern
3
3
 
4
- included { attr_accessor :use_default_scope }
4
+ included { criteria_option :use_default_scope, :merge_with => :set_scalar }
5
5
 
6
6
  def scoped
7
- chain { |criteria| criteria.use_default_scope = true }
7
+ chain(:use_default_scope => true)
8
8
  end
9
9
 
10
10
  def unscoped
11
- chain { |criteria| criteria.use_default_scope = false }
12
- end
13
-
14
- def merge!(criteria, options={})
15
- super
16
- self.use_default_scope = criteria.use_default_scope unless criteria.use_default_scope.nil?
17
- self
11
+ chain(:use_default_scope => false)
18
12
  end
19
13
 
20
14
  def respond_to?(name, include_private = false)
@@ -31,7 +25,7 @@ module NoBrainer::Criteria::Scope
31
25
  private
32
26
 
33
27
  def should_apply_default_scope?
34
- model.default_scope_proc && use_default_scope != false
28
+ model.default_scope_proc && @options[:use_default_scope] != false
35
29
  end
36
30
 
37
31
  def _apply_default_scope
@@ -1,47 +1,41 @@
1
1
  module NoBrainer::Criteria::Where
2
- extend ActiveSupport::Concern
2
+ NON_CHAINABLE_OPERATORS = %w(in nin eq ne not gt ge gte lt le lte defined).map(&:to_sym)
3
+ CHAINABLE_OPERATORS = %w(any all).map(&:to_sym)
4
+ OPERATORS = CHAINABLE_OPERATORS + NON_CHAINABLE_OPERATORS
5
+
6
+ require 'symbol_decoration'
7
+ Symbol::Decoration.register(*NON_CHAINABLE_OPERATORS)
8
+ Symbol::Decoration.register(*CHAINABLE_OPERATORS, :chainable => true)
3
9
 
4
- included { attr_accessor :where_ast, :with_index_name }
10
+ extend ActiveSupport::Concern
5
11
 
6
- def initialize(options={})
7
- super
12
+ included do
13
+ criteria_option :where_ast, :merge_with => NoBrainer::Criteria::Where.method(:merge_where_ast)
8
14
  end
9
15
 
10
16
  def where(*args, &block)
11
- chain { |criteria| criteria.where_ast = parse_clause([*args, block].compact) }
17
+ chain(:where_ast => parse_clause([*args, block].compact))
12
18
  end
13
19
 
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
20
+ def self.merge_where_ast(a, b)
21
+ (a ? MultiOperator.new(:and, [a, b]) : b).simplify
29
22
  end
30
23
 
31
24
  def where_present?
32
- finalized_criteria.where_ast.try(:clauses).present?
25
+ finalized_criteria.options[:where_ast].try(:clauses).present?
33
26
  end
34
27
 
35
28
  def where_indexed?
36
- !!where_index_name
29
+ where_index_name.present?
37
30
  end
38
31
 
39
32
  def where_index_name
40
- where_index_finder.index_name
33
+ index = where_index_finder.strategy.try(:index)
34
+ index.is_a?(Array) ? index.map(&:name) : index.try(:name)
41
35
  end
42
36
 
43
37
  def where_index_type
44
- where_index_finder.index_type
38
+ where_index_finder.strategy.try(:rql_op)
45
39
  end
46
40
 
47
41
  private
@@ -49,7 +43,7 @@ module NoBrainer::Criteria::Where
49
43
  class MultiOperator < Struct.new(:op, :clauses)
50
44
  def simplify
51
45
  clauses = self.clauses.map(&:simplify)
52
- if self.clauses.size == 1 && self.clauses.first.is_a?(self.class)
46
+ if clauses.size == 1 && clauses.first.is_a?(self.class)
53
47
  return clauses.first
54
48
  end
55
49
 
@@ -57,19 +51,19 @@ module NoBrainer::Criteria::Where
57
51
  v.is_a?(self.class) && (v.clauses.size == 1 || v.op == self.op)
58
52
  end
59
53
  simplified_clauses = other_clauses + same_op_clauses.map(&:clauses).flatten(1)
60
- simplified_clauses = BinaryOperator.simplify_clauses(op, simplified_clauses.uniq)
54
+ simplified_clauses = BinaryOperator.simplify_clauses(op, simplified_clauses.uniq)
61
55
  self.class.new(op, simplified_clauses)
62
56
  end
63
57
 
64
58
  def to_rql(doc)
65
59
  case op
66
- when :and then clauses.map { |c| c.to_rql(doc) }.reduce { |a,b| a & b }
67
- when :or then clauses.map { |c| c.to_rql(doc) }.reduce { |a,b| a | b }
60
+ when :and then clauses.map { |c| c.to_rql(doc) }.reduce(:&)
61
+ when :or then clauses.map { |c| c.to_rql(doc) }.reduce(:|)
68
62
  end
69
63
  end
70
64
  end
71
65
 
72
- class BinaryOperator < Struct.new(:key, :op, :value, :model, :casted_values)
66
+ class BinaryOperator < Struct.new(:key, :key_modifier, :op, :value, :model, :casted_values)
73
67
  def self.get_candidate_clauses(clauses, *types)
74
68
  clauses.select { |c| c.is_a?(self) && types.include?(c.op) }
75
69
  end
@@ -78,14 +72,14 @@ module NoBrainer::Criteria::Where
78
72
  # This code assumes that simplfy() has already been called on all clauses.
79
73
  if op == :or
80
74
  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
75
+ new_clauses = eq_clauses.group_by { |c| [c.key, c.key_modifier] }.map do |(key, key_modifier), clauses|
76
+ if key_modifier.in?([:scalar, :any]) && clauses.size > 1
85
77
  values = clauses.map { |c| c.op == :in ? c.value : [c.value] }.flatten(1).uniq
86
- BinaryOperator.new(key, :in, values, clauses.first.model, true)
78
+ [BinaryOperator.new(key, key_modifier, :in, values, clauses.first.model, true)]
79
+ else
80
+ clauses
87
81
  end
88
- end
82
+ end.flatten(1)
89
83
 
90
84
  if new_clauses.size != eq_clauses.size
91
85
  ast_clauses = ast_clauses - eq_clauses + new_clauses
@@ -96,29 +90,49 @@ module NoBrainer::Criteria::Where
96
90
  end
97
91
 
98
92
  def simplify
99
- key = cast_key(self.key)
100
- case op
101
- when :in then
102
- case value
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)
105
- else raise ArgumentError.new ":in takes an array/range, not #{value}"
106
- end
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)
93
+ new_key = cast_key(key)
94
+ new_op, new_value = case op
95
+ when :in then
96
+ case value
97
+ when Range then [:between, (cast_value(value.min)..cast_value(value.max))]
98
+ when Array then [:in, value.map(&method(:cast_value)).uniq]
99
+ else raise ArgumentError.new ":in takes an array/range, not #{value}"
100
+ end
101
+ when :between then [op, (cast_value(value.min)..cast_value(value.max))]
102
+ when :defined
103
+ raise "Incorrect use of `#{op}' and `#{key_modifier}'" if key_modifier != :scalar
104
+ [op, cast_value(value)]
105
+ else [op, cast_value(value)]
109
106
  end
107
+ BinaryOperator.new(new_key, key_modifier, new_op, new_value, model, true)
110
108
  end
111
109
 
112
110
  def to_rql(doc)
113
111
  key = model.lookup_field_alias(self.key)
112
+
113
+ case key_modifier
114
+ when :scalar then
115
+ case op
116
+ when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
117
+ else to_rql_scalar(doc[key])
118
+ end
119
+ when :any then doc[key].map { |lvalue| to_rql_scalar(lvalue) }.contains(true)
120
+ when :all then doc[key].map { |lvalue| to_rql_scalar(lvalue) }.contains(false).not
121
+ end
122
+ end
123
+
124
+ def to_rql_scalar(lvalue)
114
125
  case op
115
- when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
116
- when :between then (doc[key] >= value.min) & (doc[key] <= value.max)
117
- when :in then RethinkDB::RQL.new.expr(value).contains(doc[key])
118
- else doc[key].__send__(op, value)
126
+ when :between then (lvalue >= value.min) & (lvalue <= value.max)
127
+ when :in then RethinkDB::RQL.new.expr(value).contains(lvalue)
128
+ else lvalue.__send__(op, value)
119
129
  end
120
130
  end
121
131
 
132
+ def compatible_with_index?(index)
133
+ [key_modifier, index.multi].in?([[:any, true], [:scalar, false]])
134
+ end
135
+
122
136
  private
123
137
 
124
138
  def association
@@ -135,11 +149,14 @@ module NoBrainer::Criteria::Where
135
149
  case association
136
150
  when NoBrainer::Document::Association::BelongsTo::Metadata
137
151
  target_model = association.target_model
138
- opts = { :attr_name => key, :value => value, :type => target_model}
152
+ opts = { :attr_name => key, :value => value, :type => target_model }
139
153
  raise NoBrainer::Error::InvalidType.new(opts) unless value.is_a?(target_model)
140
154
  value.pk_value
141
155
  else
142
- model.cast_user_to_db_for(key, value)
156
+ case key_modifier
157
+ when :scalar then model.cast_user_to_db_for(key, value)
158
+ when :any, :all then model.cast_user_to_db_for(key, [value]).first
159
+ end
143
160
  end
144
161
  end
145
162
 
@@ -148,9 +165,14 @@ module NoBrainer::Criteria::Where
148
165
 
149
166
  case association
150
167
  when NoBrainer::Document::Association::BelongsTo::Metadata then association.foreign_key
151
- else key
168
+ else ensure_valid_key!(key); key
152
169
  end
153
170
  end
171
+
172
+ def ensure_valid_key!(key)
173
+ return if model.has_field?(key) || model.has_index?(key) || model < NoBrainer::Document::DynamicAttributes
174
+ raise NoBrainer::Error::UnknownAttribute, "`#{key}' is not a declared attribute of #{model}"
175
+ end
154
176
  end
155
177
 
156
178
  class UnaryOperator < Struct.new(:op, :value)
@@ -180,7 +202,7 @@ module NoBrainer::Criteria::Where
180
202
  when Array then MultiOperator.new(:and, clause.map { |c| parse_clause(c) })
181
203
  when Hash then MultiOperator.new(:and, clause.map { |k,v| parse_clause_stub(k,v) })
182
204
  when Proc then Lambda.new(clause)
183
- when NoBrainer::DecoratedSymbol
205
+ when Symbol::Decoration
184
206
  case clause.args.size
185
207
  when 1 then parse_clause_stub(clause, clause.args.first)
186
208
  else raise "Invalid argument: #{clause}"
@@ -195,12 +217,15 @@ module NoBrainer::Criteria::Where
195
217
  when :or then MultiOperator.new(:or, value.map { |v| parse_clause(v) })
196
218
  when :not then UnaryOperator.new(:not, parse_clause(value))
197
219
  when String, Symbol then parse_clause_stub_eq(key, value)
198
- when NoBrainer::DecoratedSymbol then
199
- case key.modifier
220
+ when Symbol::Decoration then
221
+ case key.decorator
222
+ when :any, :all then parse_clause_stub_eq(key, value)
223
+ when :not, :ne then parse_clause(:not => { key.symbol.eq => value })
200
224
  when :nin then parse_clause(:not => { key.symbol.in => value })
201
- when :ne then parse_clause(:not => { key.symbol.eq => value })
225
+ when :gte then parse_clause(key.symbol.ge => value)
226
+ when :lte then parse_clause(key.symbol.le => value)
202
227
  when :eq then parse_clause_stub_eq(key.symbol, value)
203
- else BinaryOperator.new(key.symbol, key.modifier, value, self.model)
228
+ else instantiate_binary_op(key.symbol, key.decorator, value)
204
229
  end
205
230
  else raise "Invalid key: #{key}"
206
231
  end
@@ -208,137 +233,154 @@ module NoBrainer::Criteria::Where
208
233
 
209
234
  def parse_clause_stub_eq(key, value)
210
235
  case value
211
- when Range then BinaryOperator.new(key, :between, value, self.model)
212
- when Regexp then BinaryOperator.new(key, :match, value.inspect[1..-2], self.model)
213
- else BinaryOperator.new(key, :eq, value, self.model)
236
+ when Range then instantiate_binary_op(key, :between, value)
237
+ when Regexp then instantiate_binary_op(key, :match, value.inspect[1..-2])
238
+ else instantiate_binary_op(key, :eq, value)
214
239
  end
215
240
  end
216
241
 
217
- class IndexFinder < Struct.new(:criteria, :ast, :index_name, :index_type, :rql_proc)
218
- def initialize(*args)
219
- super
242
+ def instantiate_binary_op(key, op, value)
243
+ case key
244
+ when Symbol::Decoration then BinaryOperator.new(key.symbol, key.decorator, op, value, self.model)
245
+ else BinaryOperator.new(key, :scalar, op, value, self.model)
220
246
  end
247
+ end
221
248
 
222
- def could_find_index?
223
- !!self.index_name
249
+ class IndexFinder < Struct.new(:criteria, :ast, :strategy)
250
+ class Strategy < Struct.new(:rql_op, :index, :ast, :rql_proc); end
251
+ class IndexStrategy < Struct.new(:criteria_ast, :optimized_clauses, :index, :rql_op, :rql_args, :rql_options)
252
+ def ast
253
+ MultiOperator.new(criteria_ast.op, criteria_ast.clauses - optimized_clauses)
254
+ end
255
+
256
+ def rql_proc
257
+ lambda do |rql|
258
+ opt = (rql_options || {}).merge(:index => index.aliased_name)
259
+ r = rql.__send__(rql_op, *rql_args, opt)
260
+ # TODO distinct: waiting for issue #3345
261
+ # TODO coerce_to: waiting for issue #3346
262
+ index.multi ? r.coerce_to('array').distinct : r
263
+ end
264
+ end
224
265
  end
225
266
 
226
267
  def get_candidate_clauses(*types)
227
268
  BinaryOperator.get_candidate_clauses(ast.clauses, *types)
228
269
  end
229
270
 
230
- def get_usable_indexes(*types)
231
- @usable_indexes = {}
232
- @usable_indexes[types] ||= begin
233
- indexes = criteria.model.indexes.values
234
- indexes = indexes.select { |i| types.include?(i.kind) } if types.present?
235
- if criteria.with_index_name && criteria.with_index_name != true
236
- indexes = indexes.select { |i| i.name == criteria.with_index_name.to_sym }
237
- end
238
- indexes
271
+ def get_usable_indexes(options={})
272
+ indexes = criteria.model.indexes.values
273
+ options.each { |k,v| indexes = indexes.select { |i| v == i.__send__(k) } }
274
+ if criteria.options[:use_index] && criteria.options[:use_index] != true
275
+ indexes = indexes.select { |i| i.name == criteria.options[:use_index].to_sym }
239
276
  end
277
+ indexes
240
278
  end
241
279
 
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?
245
- end
246
-
247
- def find_index_canonical
280
+ def find_strategy_canonical
248
281
  clauses = Hash[get_candidate_clauses(:eq, :in, :between).map { |c| [c.key, c] }]
249
- return unless clauses.present?
282
+ return nil unless clauses.present?
250
283
 
251
- if index = get_usable_indexes.select { |i| clauses[i.name] }.first
284
+ get_usable_indexes.each do |index|
252
285
  clause = clauses[index.name]
253
- self.index_name = index.name
254
- self.ast = remove_from_ast([clause])
255
- self.index_type = clause.op == :between ? :between : :get_all
256
- self.rql_proc = case clause.op
257
- when :eq then ->(rql){ rql.get_all(clause.value, :index => index.aliased_name) }
258
- when :in then ->(rql){ rql.get_all(*clause.value, :index => index.aliased_name) }
259
- when :between then ->(rql){ rql.between(clause.value.min, clause.value.max, :index => index.aliased_name,
260
- :left_bound => :closed, :right_bound => :closed) }
286
+ next unless clause.try(:compatible_with_index?, index)
287
+
288
+ args = case clause.op
289
+ when :eq then [:get_all, [clause.value]]
290
+ when :in then [:get_all, clause.value]
291
+ when :between then [:between, [clause.value.min, clause.value.max],
292
+ :left_bound => :closed, :right_bound => :closed]
261
293
  end
294
+ return IndexStrategy.new(ast, [clause], index, *args)
262
295
  end
296
+ return nil
263
297
  end
264
298
 
265
- def find_index_compound
299
+ def find_strategy_compound
266
300
  clauses = Hash[get_candidate_clauses(:eq).map { |c| [c.key, c] }]
267
- return unless clauses.present?
301
+ return nil unless clauses.present?
268
302
 
269
- if index = get_usable_indexes(:compound).select { |i| i.what & clauses.keys == i.what }.first
303
+ get_usable_indexes(:kind => :compound, :multi => false).each do |index|
270
304
  indexed_clauses = index.what.map { |field| clauses[field] }
271
- self.index_name = index.name
272
- self.ast = remove_from_ast(indexed_clauses)
273
- self.index_type = :get_all
274
- self.rql_proc = ->(rql){ rql.get_all(indexed_clauses.map { |c| c.value }, :index => index.aliased_name) }
305
+ next unless indexed_clauses.all? { |c| c.try(:compatible_with_index?, index) }
306
+
307
+ return IndexStrategy.new(ast, indexed_clauses, index, :get_all, [indexed_clauses.map(&:value)])
275
308
  end
309
+ return nil
276
310
  end
277
311
 
278
- def find_index_hidden_between
312
+ def find_strategy_hidden_between
279
313
  clauses = get_candidate_clauses(:gt, :ge, :lt, :le).group_by(&:key)
280
- return unless clauses.present?
314
+ return nil unless clauses.present?
281
315
 
282
- if index = get_usable_indexes.select { |i| clauses[i.name] }.first
283
- op_clauses = Hash[clauses[index.name].map { |c| [c.op, c] }]
284
- left_bound = op_clauses[:gt] || op_clauses[:ge]
316
+ get_usable_indexes.each do |index|
317
+ matched_clauses = clauses[index.name].try(:select) { |c| c.compatible_with_index?(index) }
318
+ next unless matched_clauses.present?
319
+
320
+ op_clauses = Hash[matched_clauses.map { |c| [c.op, c] }]
321
+ left_bound = op_clauses[:gt] || op_clauses[:ge]
285
322
  right_bound = op_clauses[:lt] || op_clauses[:le]
286
323
 
287
- self.index_name = index.name
288
- self.ast = remove_from_ast([left_bound, right_bound].compact)
324
+ # XXX we must keep only one bound when using `any', otherwise we get different semantics.
325
+ right_bound = nil if index.multi && left_bound && right_bound
289
326
 
290
327
  options = {}
291
- options[:index] = index.aliased_name
292
- options[:left_bound] = {:gt => :open, :ge => :closed}[left_bound.op] if left_bound
328
+ options[:left_bound] = {:gt => :open, :ge => :closed}[left_bound.op] if left_bound
293
329
  options[:right_bound] = {:lt => :open, :le => :closed}[right_bound.op] if right_bound
294
- self.index_type = :between
295
- self.rql_proc = ->(rql){ rql.between(left_bound.try(:value), right_bound.try(:value), options) }
330
+
331
+ return IndexStrategy.new(ast, [left_bound, right_bound].compact, index, :between,
332
+ [left_bound.try(:value), right_bound.try(:value)], options)
296
333
  end
334
+ return nil
297
335
  end
298
336
 
299
- def find_union_index
300
- indexes = []
301
- index_finder = self
302
-
303
- loop do
304
- index_finder = index_finder.dup
305
- break unless index_finder.find_index_canonical
306
- # TODO To use a compound index, we'd have to add all permutations in the query
307
- indexes << index_finder
308
- break unless index_finder.ast
337
+ def find_strategy_union
338
+ strategies = ast.clauses.map do |inner_ast|
339
+ inner_ast = MultiOperator.new(:and, [inner_ast]) unless inner_ast.is_a?(MultiOperator)
340
+ raise 'fatal' unless inner_ast.op == :and
341
+ self.class.new(criteria, inner_ast).find_strategy
309
342
  end
310
343
 
311
- if indexes.present? && !index_finder.ast
312
- self.ast = nil
313
- self.index_name = indexes.map(&:index_name)
314
- self.index_type = indexes.map(&:index_type)
315
- self.rql_proc = ->(rql){ indexes.map { |index| index.rql_proc.call(rql) }.reduce { |a,b| a.union(b) } }
344
+ return nil if strategies.any?(&:nil?)
345
+
346
+ rql_proc = lambda do |base_rql|
347
+ strategies.map do |strategy|
348
+ rql = strategy.rql_proc.call(base_rql)
349
+ rql = rql.filter { |doc| strategy.ast.to_rql(doc) } if strategy.ast.try(:clauses).present?
350
+ rql
351
+ end.reduce(:union).distinct
316
352
  end
353
+
354
+ Strategy.new(:union, strategies.map(&:index), nil, rql_proc)
317
355
  end
318
356
 
319
- def find_index
320
- return if ast.nil? || criteria.without_index?
357
+ def find_strategy
358
+ return nil unless ast.try(:clauses).present? && !criteria.without_index?
321
359
  case ast.op
322
- when :and then find_index_compound || find_index_canonical || find_index_hidden_between
323
- when :or then find_union_index
360
+ when :and then find_strategy_compound || find_strategy_canonical || find_strategy_hidden_between
361
+ when :or then find_strategy_union
324
362
  end
325
363
  end
364
+
365
+ def find_strategy!
366
+ self.strategy = find_strategy
367
+ end
326
368
  end
327
369
 
328
370
  def where_index_finder
329
371
  return finalized_criteria.__send__(:where_index_finder) unless finalized?
330
- @where_index_finder ||= IndexFinder.new(self, where_ast).tap { |index_finder| index_finder.find_index }
372
+ @where_index_finder ||= IndexFinder.new(self, @options[:where_ast]).tap(&:find_strategy!)
331
373
  end
332
374
 
333
375
  def compile_rql_pass1
334
376
  rql = super
335
- rql = where_index_finder.rql_proc.call(rql) if where_index_finder.could_find_index?
377
+ rql = where_index_finder.strategy.rql_proc.call(rql) if where_indexed?
336
378
  rql
337
379
  end
338
380
 
339
381
  def compile_rql_pass2
340
382
  rql = super
341
- ast = where_index_finder.could_find_index? ? where_index_finder.ast : self.where_ast
383
+ ast = where_indexed? ? where_index_finder.strategy.ast : @options[:where_ast]
342
384
  rql = rql.filter { |doc| ast.to_rql(doc) } if ast.try(:clauses).present?
343
385
  rql
344
386
  end
@@ -12,9 +12,17 @@ module NoBrainer::Document::Aliases
12
12
 
13
13
  module ClassMethods
14
14
  def _field(attr, options={})
15
+ if options[:store_as]
16
+ self.alias_map[attr.to_s] = options[:store_as].to_s
17
+ self.alias_reverse_map[options[:store_as].to_s] = attr.to_s
18
+ end
19
+ super
20
+ end
21
+
22
+ def field(attr, options={})
15
23
  if options[:as]
16
- self.alias_map[attr.to_s] = options[:as].to_s
17
- self.alias_reverse_map[options[:as].to_s] = attr.to_s
24
+ STDERR.puts "[NoBrainer] `:as' is deprecated and will be removed. Please use `:store_as' instead (from the #{self} model)"
25
+ options[:store_as] = options.delete(:as)
18
26
  end
19
27
  super
20
28
  end
@@ -2,7 +2,7 @@ class NoBrainer::Document::Association::BelongsTo
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :foreign_key_as, :index, :validates, :required]
5
+ VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :foreign_key_store_as, :index, :validates, :required]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
  extend NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
@@ -29,8 +29,8 @@ class NoBrainer::Document::Association::BelongsTo
29
29
  # This would have the effect of loading all the models because they
30
30
  # are likely to be related to each other. So we don't know the type
31
31
  # of the primary key of the target.
32
- owner_model.field(foreign_key, :as => options[:foreign_key_as], :index => options[:index])
33
- owner_model.validates(target_name, { :presence => true }) if options[:required]
32
+ owner_model.field(foreign_key, :store_as => options[:foreign_key_store_as], :index => options[:index])
33
+ owner_model.validates(target_name, :presence => options[:required]) if options[:required]
34
34
  owner_model.validates(target_name, options[:validates]) if options[:validates]
35
35
 
36
36
  delegate("#{foreign_key}=", :assign_foreign_key, :call_super => true)
@@ -37,7 +37,7 @@ module NoBrainer::Document::Association::Core
37
37
  end
38
38
 
39
39
  def add_callback_for(what)
40
- instance_eval <<-RUBY, __FILE__, __LINE__+1
40
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
41
41
  if !@added_#{what}
42
42
  metadata = self
43
43
  owner_model.#{what} { associations[metadata].#{what}_callback }
@@ -17,7 +17,7 @@ class NoBrainer::Document::Association::EagerLoader
17
17
  if owner_keys.present?
18
18
  targets = criteria.where(target_key.in => owner_keys)
19
19
  .map { |target| [target.read_attribute(target_key), target] }
20
- .reduce(Hash.new { |k,v| k[v] = [] }) { |h,(k,v)| h[k] << v; h }
20
+ .each_with_object(Hash.new { |k,v| k[v] = [] }) { |(k,v),h| h[k] << v }
21
21
 
22
22
  unloaded_docs.each do |doc|
23
23
  doc_targets = targets[doc.read_attribute(owner_key)]
@@ -45,14 +45,15 @@ class NoBrainer::Document::Association::EagerLoader
45
45
  when Hash then preloads.each do |k,v|
46
46
  if v.is_a?(NoBrainer::Criteria)
47
47
  v = v.dup
48
- nested_preloads, v._preloads = v._preloads, []
48
+ nested_preloads, v.options[:preload] = v.options[:preload], []
49
49
  eager_load(eager_load_association(docs, k, v), nested_preloads)
50
50
  else
51
51
  eager_load(eager_load_association(docs, k), v)
52
52
  end
53
53
  end
54
54
  when Array then preloads.each { |v| eager_load(docs, v) }
55
- else eager_load_association(docs, preloads)
55
+ when nil then;
56
+ else eager_load_association(docs, preloads) # String and Symbol
56
57
  end
57
58
  true
58
59
  end
@@ -53,7 +53,7 @@ class NoBrainer::Document::Association::HasMany
53
53
  end
54
54
 
55
55
  def write(new_children)
56
- raise "You can't assign #{target_name}. " +
56
+ raise "You can't assign #{target_name}. " \
57
57
  "Instead, you must modify delete and create #{target_model} manually."
58
58
  end
59
59
 
@@ -80,7 +80,7 @@ class NoBrainer::Document::Association::HasMany
80
80
  end
81
81
 
82
82
  def set_inverse_proc
83
- lambda { |target| set_inverses_of([target]) if target.is_a?(NoBrainer::Document) }
83
+ ->(target){ set_inverses_of([target]) if target.is_a?(NoBrainer::Document) }
84
84
  end
85
85
 
86
86
  def before_destroy_callback