nobrainer 0.18.0 → 0.22.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +0 -5
  3. data/lib/no_brainer/config.rb +73 -39
  4. data/lib/no_brainer/connection.rb +2 -4
  5. data/lib/no_brainer/criteria/after_find.rb +3 -11
  6. data/lib/no_brainer/criteria/aggregate.rb +2 -2
  7. data/lib/no_brainer/criteria/cache.rb +15 -10
  8. data/lib/no_brainer/criteria/core.rb +46 -11
  9. data/lib/no_brainer/criteria/delete.rb +2 -2
  10. data/lib/no_brainer/criteria/eager_load.rb +51 -0
  11. data/lib/no_brainer/criteria/extend.rb +4 -16
  12. data/lib/no_brainer/criteria/find.rb +27 -0
  13. data/lib/no_brainer/criteria/index.rb +7 -13
  14. data/lib/no_brainer/criteria/limit.rb +5 -12
  15. data/lib/no_brainer/criteria/order_by.rb +20 -36
  16. data/lib/no_brainer/criteria/pluck.rb +16 -22
  17. data/lib/no_brainer/criteria/raw.rb +4 -10
  18. data/lib/no_brainer/criteria/scope.rb +6 -19
  19. data/lib/no_brainer/criteria/update.rb +8 -6
  20. data/lib/no_brainer/criteria/where.rb +252 -138
  21. data/lib/no_brainer/criteria.rb +3 -2
  22. data/lib/no_brainer/document/aliases.rb +3 -3
  23. data/lib/no_brainer/document/association/belongs_to.rb +9 -5
  24. data/lib/no_brainer/document/association/core.rb +6 -5
  25. data/lib/no_brainer/document/association/eager_loader.rb +9 -9
  26. data/lib/no_brainer/document/association/has_many.rb +23 -9
  27. data/lib/no_brainer/document/association/has_many_through.rb +12 -3
  28. data/lib/no_brainer/document/atomic_ops.rb +79 -78
  29. data/lib/no_brainer/document/attributes.rb +24 -20
  30. data/lib/no_brainer/document/callbacks.rb +1 -1
  31. data/lib/no_brainer/document/core.rb +5 -2
  32. data/lib/no_brainer/document/criteria.rb +14 -19
  33. data/lib/no_brainer/document/dirty.rb +11 -16
  34. data/lib/no_brainer/document/index/index.rb +2 -1
  35. data/lib/no_brainer/document/index/meta_store.rb +1 -1
  36. data/lib/no_brainer/document/index.rb +14 -10
  37. data/lib/no_brainer/document/persistance.rb +24 -13
  38. data/lib/no_brainer/document/primary_key/generator.rb +83 -0
  39. data/lib/no_brainer/document/{id.rb → primary_key.rb} +9 -36
  40. data/lib/no_brainer/document/store_in.rb +2 -2
  41. data/lib/no_brainer/document/timestamps.rb +4 -2
  42. data/lib/no_brainer/document/types/binary.rb +2 -7
  43. data/lib/no_brainer/document/types/boolean.rb +2 -4
  44. data/lib/no_brainer/document/types/date.rb +2 -2
  45. data/lib/no_brainer/document/types/float.rb +2 -2
  46. data/lib/no_brainer/document/types/geo.rb +1 -0
  47. data/lib/no_brainer/document/types/integer.rb +2 -2
  48. data/lib/no_brainer/document/types/set.rb +2 -2
  49. data/lib/no_brainer/document/types/string.rb +5 -2
  50. data/lib/no_brainer/document/types/symbol.rb +2 -2
  51. data/lib/no_brainer/document/types/text.rb +18 -0
  52. data/lib/no_brainer/document/types/time.rb +2 -2
  53. data/lib/no_brainer/document/types.rb +17 -18
  54. data/lib/no_brainer/document/validation/not_null.rb +15 -0
  55. data/lib/no_brainer/document/{uniqueness.rb → validation/uniqueness.rb} +11 -11
  56. data/lib/no_brainer/document/validation.rb +35 -6
  57. data/lib/no_brainer/document.rb +1 -1
  58. data/lib/no_brainer/error.rb +21 -19
  59. data/lib/no_brainer/geo/base.rb +16 -0
  60. data/lib/no_brainer/geo/circle.rb +25 -0
  61. data/lib/no_brainer/geo/line_string.rb +11 -0
  62. data/lib/no_brainer/geo/point.rb +49 -0
  63. data/lib/no_brainer/geo/polygon.rb +11 -0
  64. data/lib/no_brainer/geo.rb +4 -0
  65. data/lib/no_brainer/locale/en.yml +1 -0
  66. data/lib/no_brainer/lock.rb +114 -0
  67. data/lib/no_brainer/query_runner/connection_lock.rb +1 -1
  68. data/lib/no_brainer/query_runner/database_on_demand.rb +0 -1
  69. data/lib/no_brainer/query_runner/missing_index.rb +1 -1
  70. data/lib/no_brainer/query_runner/reconnect.rb +9 -11
  71. data/lib/no_brainer/query_runner/run_options.rb +0 -3
  72. data/lib/no_brainer/query_runner/table_on_demand.rb +3 -4
  73. data/lib/no_brainer/railtie/database.rake +2 -2
  74. data/lib/no_brainer/rql.rb +1 -5
  75. data/lib/nobrainer.rb +2 -6
  76. data/lib/rails/generators/nobrainer.rb +1 -1
  77. metadata +34 -9
  78. data/lib/no_brainer/criteria/preload.rb +0 -50
  79. data/lib/no_brainer/decorated_symbol.rb +0 -17
@@ -1,47 +1,48 @@
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 near intersects).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
11
+ include ActiveModel::ForbiddenAttributesProtection
5
12
 
6
- def initialize(options={})
7
- super
13
+ included do
14
+ criteria_option :where_ast, :merge_with => NoBrainer::Criteria::Where.method(:merge_where_ast)
15
+ criteria_option :without_distinct, :merge_with => :set_scalar
8
16
  end
9
17
 
10
18
  def where(*args, &block)
11
- chain { |criteria| criteria.where_ast = parse_clause([*args, block].compact) }
19
+ chain(:where_ast => parse_clause([*args, block].compact))
12
20
  end
13
21
 
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
22
+ def self.merge_where_ast(a, b)
23
+ (a ? MultiOperator.new(:and, [a, b]) : b).simplify
29
24
  end
30
25
 
31
26
  def where_present?
32
- finalized_criteria.where_ast.try(:clauses).present?
27
+ finalized_criteria.options[:where_ast].try(:clauses).present?
33
28
  end
34
29
 
35
30
  def where_indexed?
36
- !!where_index_name
31
+ where_index_name.present?
37
32
  end
38
33
 
39
34
  def where_index_name
40
- where_index_finder.index_name
35
+ index = where_index_finder.strategy.try(:index)
36
+ index.is_a?(Array) ? index.map(&:name) : index.try(:name)
41
37
  end
42
38
 
43
39
  def where_index_type
44
- where_index_finder.index_type
40
+ where_index_finder.strategy.try(:rql_op)
41
+ end
42
+
43
+ def without_distinct(value = true)
44
+ # helper for delete_all which can't operate on distinct
45
+ chain(:without_distinct => value)
45
46
  end
46
47
 
47
48
  private
@@ -49,7 +50,7 @@ module NoBrainer::Criteria::Where
49
50
  class MultiOperator < Struct.new(:op, :clauses)
50
51
  def simplify
51
52
  clauses = self.clauses.map(&:simplify)
52
- if self.clauses.size == 1 && self.clauses.first.is_a?(self.class)
53
+ if clauses.size == 1 && clauses.first.is_a?(self.class)
53
54
  return clauses.first
54
55
  end
55
56
 
@@ -57,19 +58,19 @@ module NoBrainer::Criteria::Where
57
58
  v.is_a?(self.class) && (v.clauses.size == 1 || v.op == self.op)
58
59
  end
59
60
  simplified_clauses = other_clauses + same_op_clauses.map(&:clauses).flatten(1)
60
- simplified_clauses = BinaryOperator.simplify_clauses(op, simplified_clauses.uniq)
61
+ simplified_clauses = BinaryOperator.simplify_clauses(op, simplified_clauses.uniq)
61
62
  self.class.new(op, simplified_clauses)
62
63
  end
63
64
 
64
65
  def to_rql(doc)
65
66
  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 }
67
+ when :and then clauses.map { |c| c.to_rql(doc) }.reduce(:&)
68
+ when :or then clauses.map { |c| c.to_rql(doc) }.reduce(:|)
68
69
  end
69
70
  end
70
71
  end
71
72
 
72
- class BinaryOperator < Struct.new(:key, :op, :value, :model, :casted_values)
73
+ class BinaryOperator < Struct.new(:key, :key_modifier, :op, :value, :model, :casted_values)
73
74
  def self.get_candidate_clauses(clauses, *types)
74
75
  clauses.select { |c| c.is_a?(self) && types.include?(c.op) }
75
76
  end
@@ -78,14 +79,14 @@ module NoBrainer::Criteria::Where
78
79
  # This code assumes that simplfy() has already been called on all clauses.
79
80
  if op == :or
80
81
  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
82
+ new_clauses = eq_clauses.group_by { |c| [c.key, c.key_modifier] }.map do |(key, key_modifier), clauses|
83
+ if key_modifier.in?([:scalar, :any]) && clauses.size > 1
85
84
  values = clauses.map { |c| c.op == :in ? c.value : [c.value] }.flatten(1).uniq
86
- BinaryOperator.new(key, :in, values, clauses.first.model, true)
85
+ [BinaryOperator.new(key, key_modifier, :in, values, clauses.first.model, true)]
86
+ else
87
+ clauses
87
88
  end
88
- end
89
+ end.flatten(1)
89
90
 
90
91
  if new_clauses.size != eq_clauses.size
91
92
  ast_clauses = ast_clauses - eq_clauses + new_clauses
@@ -96,29 +97,57 @@ module NoBrainer::Criteria::Where
96
97
  end
97
98
 
98
99
  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)
100
+ new_key = cast_key(key)
101
+ new_op, new_value = case op
102
+ when :in then
103
+ case value
104
+ when Range then [:between, (cast_value(value.min)..cast_value(value.max))]
105
+ when Array then [:in, value.map(&method(:cast_value)).uniq]
106
+ else raise ArgumentError.new "`in' takes an array/range, not #{value}"
107
+ end
108
+ when :between then [op, (cast_value(value.min)..cast_value(value.max))]
109
+ when :defined
110
+ raise "Incorrect use of `#{op}' and `#{key_modifier}'" if key_modifier != :scalar
111
+ [op, cast_value(value)]
112
+ else [op, cast_value(value)]
109
113
  end
114
+ BinaryOperator.new(new_key, key_modifier, new_op, new_value, model, true)
110
115
  end
111
116
 
112
117
  def to_rql(doc)
113
118
  key = model.lookup_field_alias(self.key)
119
+
120
+ case key_modifier
121
+ when :scalar then
122
+ case op
123
+ when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
124
+ else to_rql_scalar(doc[key])
125
+ end
126
+ when :any then doc[key].map { |lvalue| to_rql_scalar(lvalue) }.contains(true)
127
+ when :all then doc[key].map { |lvalue| to_rql_scalar(lvalue) }.contains(false).not
128
+ end
129
+ end
130
+
131
+ def to_rql_scalar(lvalue)
114
132
  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)
133
+ when :between then (lvalue >= value.min) & (lvalue <= value.max)
134
+ when :in then RethinkDB::RQL.new.expr(value).contains(lvalue)
135
+ when :intersects then lvalue.intersects(value.to_rql)
136
+ when :near
137
+ options = value.dup
138
+ point = options.delete(:point)
139
+ max_dist = options.delete(:max_dist)
140
+ # XXX max_results is not used, seems to be a workaround of rethinkdb index implemetnation.
141
+ _ = options.delete(:max_results)
142
+ RethinkDB::RQL.new.distance(lvalue, point.to_rql, options) <= max_dist
143
+ else lvalue.__send__(op, value)
119
144
  end
120
145
  end
121
146
 
147
+ def compatible_with_index?(index)
148
+ [key_modifier, index.multi].in?([[:any, true], [:scalar, false]])
149
+ end
150
+
122
151
  private
123
152
 
124
153
  def association
@@ -135,11 +164,36 @@ module NoBrainer::Criteria::Where
135
164
  case association
136
165
  when NoBrainer::Document::Association::BelongsTo::Metadata
137
166
  target_model = association.target_model
138
- opts = { :attr_name => key, :value => value, :type => target_model}
139
- raise NoBrainer::Error::InvalidType.new(opts) unless value.is_a?(target_model)
167
+ unless value.is_a?(target_model)
168
+ opts = { :model => model, :attr_name => key, :type => target_model, :value => value }
169
+ raise NoBrainer::Error::InvalidType.new(opts)
170
+ end
140
171
  value.pk_value
141
172
  else
142
- model.cast_user_to_db_for(key, value)
173
+ case op
174
+ when :defined then NoBrainer::Boolean.nobrainer_cast_user_to_model(value)
175
+ when :intersects
176
+ raise "Use a geo object with `intersects`" unless value.is_a?(NoBrainer::Geo::Base)
177
+ value
178
+ when :near
179
+ raise "Incorrect use of `near': rvalue must be a hash" unless value.is_a?(Hash)
180
+ options = NoBrainer::Geo::Base.normalize_geo_options(value)
181
+
182
+ unless options[:point] && options[:max_dist]
183
+ raise "`near' takes something like {:point => P, :max_distance => d}"
184
+ end
185
+
186
+ unless options[:point].is_a?(NoBrainer::Geo::Point)
187
+ options[:point] = NoBrainer::Geo::Point.nobrainer_cast_user_to_model(options[:point])
188
+ end
189
+
190
+ options
191
+ else
192
+ case key_modifier
193
+ when :scalar then model.cast_user_to_db_for(key, value)
194
+ when :any, :all then model.cast_user_to_db_for(key, [value]).first
195
+ end
196
+ end
143
197
  end
144
198
  end
145
199
 
@@ -148,9 +202,14 @@ module NoBrainer::Criteria::Where
148
202
 
149
203
  case association
150
204
  when NoBrainer::Document::Association::BelongsTo::Metadata then association.foreign_key
151
- else key
205
+ else ensure_valid_key!(key); key
152
206
  end
153
207
  end
208
+
209
+ def ensure_valid_key!(key)
210
+ return if model.has_field?(key) || model.has_index?(key) || model < NoBrainer::Document::DynamicAttributes
211
+ raise NoBrainer::Error::UnknownAttribute, "`#{key}' is not a declared attribute of #{model}"
212
+ end
154
213
  end
155
214
 
156
215
  class UnaryOperator < Struct.new(:op, :value)
@@ -176,11 +235,12 @@ module NoBrainer::Criteria::Where
176
235
  end
177
236
 
178
237
  def parse_clause(clause)
238
+ clause = sanitize_for_mass_assignment(clause)
179
239
  case clause
180
240
  when Array then MultiOperator.new(:and, clause.map { |c| parse_clause(c) })
181
241
  when Hash then MultiOperator.new(:and, clause.map { |k,v| parse_clause_stub(k,v) })
182
242
  when Proc then Lambda.new(clause)
183
- when NoBrainer::DecoratedSymbol
243
+ when Symbol::Decoration
184
244
  case clause.args.size
185
245
  when 1 then parse_clause_stub(clause, clause.args.first)
186
246
  else raise "Invalid argument: #{clause}"
@@ -191,154 +251,208 @@ module NoBrainer::Criteria::Where
191
251
 
192
252
  def parse_clause_stub(key, value)
193
253
  case key
194
- when :and then MultiOperator.new(:and, value.map { |v| parse_clause(v) })
195
- when :or then MultiOperator.new(:or, value.map { |v| parse_clause(v) })
196
- when :not then UnaryOperator.new(:not, parse_clause(value))
254
+ when :and then parse_multi_value(:and, value)
255
+ when :or then parse_multi_value(:or, value)
256
+ when :_and then parse_multi_value(:and, value, :safe => true)
257
+ when :_or then parse_multi_value(:or, value, :safe => true)
258
+ when :not then UnaryOperator.new(:not, parse_clause(value))
197
259
  when String, Symbol then parse_clause_stub_eq(key, value)
198
- when NoBrainer::DecoratedSymbol then
199
- case key.modifier
260
+ when Symbol::Decoration then
261
+ case key.decorator
262
+ when :any, :all then parse_clause_stub_eq(key, value)
263
+ when :not, :ne then parse_clause(:not => { key.symbol.eq => value })
200
264
  when :nin then parse_clause(:not => { key.symbol.in => value })
201
- when :ne then parse_clause(:not => { key.symbol.eq => value })
265
+ when :gte then parse_clause(key.symbol.ge => value)
266
+ when :lte then parse_clause(key.symbol.le => value)
202
267
  when :eq then parse_clause_stub_eq(key.symbol, value)
203
- else BinaryOperator.new(key.symbol, key.modifier, value, self.model)
268
+ else instantiate_binary_op(key.symbol, key.decorator, value)
204
269
  end
205
270
  else raise "Invalid key: #{key}"
206
271
  end
207
272
  end
208
273
 
274
+ def parse_multi_value(op, value, options={})
275
+ raise "The `#{op}' operator takes an array as argument" unless value.is_a?(Array)
276
+ if value.size == 1 && value.first.is_a?(Hash) && !options[:safe]
277
+ raise "The `#{op}' operator was provided an array with a single hash element.\n" +
278
+ "In Ruby, [:a => :b, :c => :d] means [{:a => :b, :c => :d}] which is not the same as [{:a => :b}, {:c => :d}].\n" +
279
+ "To prevent mistakes, the former construct is prohibited as you probably mean the latter.\n" +
280
+ "However, if you know what you are doing, you can use the `_#{op}' operator instead."
281
+ end
282
+ MultiOperator.new(op, value.map { |v| parse_clause(v) })
283
+ end
284
+
209
285
  def parse_clause_stub_eq(key, value)
210
286
  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)
287
+ when Range then instantiate_binary_op(key, :between, value)
288
+ when Regexp then instantiate_binary_op(key, :match, translate_regexp_to_re2_syntax(value))
289
+ else instantiate_binary_op(key, :eq, value)
214
290
  end
215
291
  end
216
292
 
217
- class IndexFinder < Struct.new(:criteria, :ast, :index_name, :index_type, :rql_proc)
218
- def initialize(*args)
219
- super
293
+ def translate_regexp_to_re2_syntax(value)
294
+ # Ruby always uses what RE2 calls "multiline mode" (the "m" flag),
295
+ # meaning that "foo\nbar" matches /^bar$/.
296
+ #
297
+ # Ruby's /m modifier means that . matches \n and corresponds to RE2's "s" flag.
298
+
299
+ flags = "m"
300
+ flags << "s" if value.options & Regexp::MULTILINE != 0
301
+ flags << "i" if value.options & Regexp::IGNORECASE != 0
302
+
303
+ "(?#{flags})#{value.source}"
304
+ end
305
+
306
+ def instantiate_binary_op(key, op, value)
307
+ case key
308
+ when Symbol::Decoration then BinaryOperator.new(key.symbol, key.decorator, op, value, self.model)
309
+ else BinaryOperator.new(key, :scalar, op, value, self.model)
220
310
  end
311
+ end
312
+
313
+ class IndexFinder < Struct.new(:criteria, :ast, :strategy)
314
+ class Strategy < Struct.new(:index_finder, :rql_op, :index, :ast, :rql_proc); end
315
+ class IndexStrategy < Struct.new(:index_finder, :criteria_ast, :optimized_clauses, :index, :rql_op, :rql_args, :rql_options)
316
+ def ast
317
+ MultiOperator.new(criteria_ast.op, criteria_ast.clauses - optimized_clauses)
318
+ end
221
319
 
222
- def could_find_index?
223
- !!self.index_name
320
+ def rql_proc
321
+ lambda do |rql|
322
+ opt = (rql_options || {}).merge(:index => index.aliased_name)
323
+ r = rql.__send__(rql_op, *rql_args, opt)
324
+ r = r.map { |i| i['doc'] } if rql_op == :get_nearest
325
+ # TODO distinct: waiting for issue #3345
326
+ # TODO coerce_to: waiting for issue #3346
327
+ r = r.coerce_to('array').distinct if index.multi && !index_finder.criteria.options[:without_distinct]
328
+ r
329
+ end
330
+ end
224
331
  end
225
332
 
226
333
  def get_candidate_clauses(*types)
227
334
  BinaryOperator.get_candidate_clauses(ast.clauses, *types)
228
335
  end
229
336
 
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
337
+ def get_usable_indexes(options={})
338
+ indexes = criteria.model.indexes.values
339
+ options.each { |k,v| indexes = indexes.select { |i| v == i.__send__(k) } }
340
+ if criteria.options[:use_index] && criteria.options[:use_index] != true
341
+ indexes = indexes.select { |i| i.name == criteria.options[:use_index].to_sym }
239
342
  end
343
+ indexes
240
344
  end
241
345
 
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
248
- clauses = Hash[get_candidate_clauses(:eq, :in, :between).map { |c| [c.key, c] }]
249
- return unless clauses.present?
250
-
251
- if index = get_usable_indexes.select { |i| clauses[i.name] }.first
252
- 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) }
346
+ def find_strategy_canonical
347
+ clauses = get_candidate_clauses(:eq, :in, :between, :near, :intersects)
348
+ return nil unless clauses.present?
349
+
350
+ usable_indexes = Hash[get_usable_indexes.map { |i| [i.name, i] }]
351
+ clauses.map do |clause|
352
+ index = usable_indexes[clause.key]
353
+ next unless index && clause.compatible_with_index?(index)
354
+ next unless index.geo == [:near, :intersects].include?(clause.op)
355
+
356
+ args = case clause.op
357
+ when :intersects then [:get_intersecting, clause.value.to_rql]
358
+ when :near
359
+ options = clause.value.dup
360
+ point = options.delete(:point)
361
+ [:get_nearest, point.to_rql, options]
362
+ when :eq then [:get_all, [clause.value]]
363
+ when :in then [:get_all, clause.value]
364
+ when :between then [:between, [clause.value.min, clause.value.max],
365
+ :left_bound => :closed, :right_bound => :closed]
261
366
  end
262
- end
367
+ IndexStrategy.new(self, ast, [clause], index, *args)
368
+ end.compact.sort_by { |strat| usable_indexes.values.index(strat.index) }.first
263
369
  end
264
370
 
265
- def find_index_compound
371
+ def find_strategy_compound
266
372
  clauses = Hash[get_candidate_clauses(:eq).map { |c| [c.key, c] }]
267
- return unless clauses.present?
373
+ return nil unless clauses.present?
268
374
 
269
- if index = get_usable_indexes(:compound).select { |i| i.what & clauses.keys == i.what }.first
375
+ get_usable_indexes(:kind => :compound, :geo => false, :multi => false).each do |index|
270
376
  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) }
377
+ next unless indexed_clauses.all? { |c| c.try(:compatible_with_index?, index) }
378
+
379
+ return IndexStrategy.new(self, ast, indexed_clauses, index, :get_all, [indexed_clauses.map(&:value)])
275
380
  end
381
+ return nil
276
382
  end
277
383
 
278
- def find_index_hidden_between
384
+ def find_strategy_hidden_between
279
385
  clauses = get_candidate_clauses(:gt, :ge, :lt, :le).group_by(&:key)
280
- return unless clauses.present?
386
+ return nil unless clauses.present?
281
387
 
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]
388
+ get_usable_indexes(:geo => false).each do |index|
389
+ matched_clauses = clauses[index.name].try(:select) { |c| c.compatible_with_index?(index) }
390
+ next unless matched_clauses.present?
391
+
392
+ op_clauses = Hash[matched_clauses.map { |c| [c.op, c] }]
393
+ left_bound = op_clauses[:gt] || op_clauses[:ge]
285
394
  right_bound = op_clauses[:lt] || op_clauses[:le]
286
395
 
287
- self.index_name = index.name
288
- self.ast = remove_from_ast([left_bound, right_bound].compact)
396
+ # XXX we must keep only one bound when using `any', otherwise we get different semantics.
397
+ right_bound = nil if index.multi && left_bound && right_bound
289
398
 
290
399
  options = {}
291
- options[:index] = index.aliased_name
292
- options[:left_bound] = {:gt => :open, :ge => :closed}[left_bound.op] if left_bound
400
+ options[:left_bound] = {:gt => :open, :ge => :closed}[left_bound.op] if left_bound
293
401
  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) }
402
+
403
+ return IndexStrategy.new(self, ast, [left_bound, right_bound].compact, index, :between,
404
+ [left_bound.try(:value), right_bound.try(:value)], options)
296
405
  end
406
+ return nil
297
407
  end
298
408
 
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
409
+ def find_strategy_union
410
+ strategies = ast.clauses.map do |inner_ast|
411
+ inner_ast = MultiOperator.new(:and, [inner_ast]) unless inner_ast.is_a?(MultiOperator)
412
+ raise 'fatal' unless inner_ast.op == :and
413
+ self.class.new(criteria, inner_ast).find_strategy
309
414
  end
310
415
 
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) } }
416
+ return nil if strategies.any?(&:nil?)
417
+
418
+ rql_proc = lambda do |base_rql|
419
+ strategies.map do |strategy|
420
+ rql = strategy.rql_proc.call(base_rql)
421
+ rql = rql.filter { |doc| strategy.ast.to_rql(doc) } if strategy.ast.try(:clauses).present?
422
+ rql
423
+ end.reduce(:union).distinct
316
424
  end
425
+
426
+ Strategy.new(self, :union, strategies.map(&:index), nil, rql_proc)
317
427
  end
318
428
 
319
- def find_index
320
- return if ast.nil? || criteria.without_index?
429
+ def find_strategy
430
+ return nil unless ast.try(:clauses).present? && !criteria.without_index?
321
431
  case ast.op
322
- when :and then find_index_compound || find_index_canonical || find_index_hidden_between
323
- when :or then find_union_index
432
+ when :and then find_strategy_compound || find_strategy_canonical || find_strategy_hidden_between
433
+ when :or then find_strategy_union
324
434
  end
325
435
  end
436
+
437
+ def find_strategy!
438
+ self.strategy = find_strategy
439
+ end
326
440
  end
327
441
 
328
442
  def where_index_finder
329
443
  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 }
444
+ @where_index_finder ||= IndexFinder.new(self, @options[:where_ast]).tap(&:find_strategy!)
331
445
  end
332
446
 
333
447
  def compile_rql_pass1
334
448
  rql = super
335
- rql = where_index_finder.rql_proc.call(rql) if where_index_finder.could_find_index?
449
+ rql = where_index_finder.strategy.rql_proc.call(rql) if where_indexed?
336
450
  rql
337
451
  end
338
452
 
339
453
  def compile_rql_pass2
340
454
  rql = super
341
- ast = where_index_finder.could_find_index? ? where_index_finder.ast : self.where_ast
455
+ ast = where_indexed? ? where_index_finder.strategy.ast : @options[:where_ast]
342
456
  rql = rql.filter { |doc| ast.to_rql(doc) } if ast.try(:clauses).present?
343
457
  rql
344
458
  end
@@ -3,6 +3,7 @@ require 'rethinkdb'
3
3
  class NoBrainer::Criteria
4
4
  extend NoBrainer::Autoload
5
5
  autoload_and_include :Core, :Raw, :AfterFind, :Where, :OrderBy, :Limit,
6
- :Pluck, :Count, :Delete, :Enumerable, :First, :Aggregate,
7
- :Preload, :Update, :Cache, :Index, :Extend, :Scope
6
+ :Pluck, :Count, :Delete, :Enumerable, :First, :Find,
7
+ :Aggregate, :EagerLoad, :Update, :Cache, :Index,
8
+ :Extend, :Scope
8
9
  end
@@ -12,9 +12,9 @@ module NoBrainer::Document::Aliases
12
12
 
13
13
  module ClassMethods
14
14
  def _field(attr, options={})
15
- 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
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
18
  end
19
19
  super
20
20
  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
 
@@ -21,6 +21,10 @@ class NoBrainer::Document::Association::BelongsTo
21
21
  (options[:class_name] || target_name.to_s.camelize).constantize
22
22
  end
23
23
 
24
+ def base_criteria
25
+ target_model.unscoped
26
+ end
27
+
24
28
  def hook
25
29
  super
26
30
 
@@ -29,16 +33,16 @@ class NoBrainer::Document::Association::BelongsTo
29
33
  # This would have the effect of loading all the models because they
30
34
  # are likely to be related to each other. So we don't know the type
31
35
  # 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]
36
+ owner_model.field(foreign_key, :store_as => options[:foreign_key_store_as], :index => options[:index])
37
+ owner_model.validates(target_name, :presence => options[:required]) if options[:required]
34
38
  owner_model.validates(target_name, options[:validates]) if options[:validates]
35
39
 
36
40
  delegate("#{foreign_key}=", :assign_foreign_key, :call_super => true)
41
+ delegate("#{target_name}_changed?", "#{foreign_key}_changed?", :to => :self)
37
42
  add_callback_for(:after_validation)
38
43
  end
39
44
 
40
- eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ primary_key },
41
- :unscoped => true
45
+ eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ primary_key }
42
46
  end
43
47
 
44
48
  # Note:
@@ -20,12 +20,13 @@ module NoBrainer::Document::Association::Core
20
20
  association_model.new(self, owner)
21
21
  end
22
22
 
23
- def delegate(method_name, target, options={})
23
+ def delegate(method_src, method_dst, options={})
24
24
  metadata = self
25
25
  owner_model.inject_in_layer :associations do
26
- define_method(method_name) do |*args, &block|
26
+ define_method(method_src) do |*args, &block|
27
27
  super(*args, &block) if options[:call_super]
28
- associations[metadata].__send__(target, *args, &block)
28
+ target = options[:to] == :self ? self : associations[metadata]
29
+ target.__send__(method_dst, *args, &block)
29
30
  end
30
31
  end
31
32
  end
@@ -37,7 +38,7 @@ module NoBrainer::Document::Association::Core
37
38
  end
38
39
 
39
40
  def add_callback_for(what)
40
- instance_eval <<-RUBY, __FILE__, __LINE__+1
41
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
41
42
  if !@added_#{what}
42
43
  metadata = self
43
44
  owner_model.#{what} { associations[metadata].#{what}_callback }
@@ -49,7 +50,7 @@ module NoBrainer::Document::Association::Core
49
50
 
50
51
  included { attr_accessor :metadata, :owner }
51
52
 
52
- delegate :primary_key, :foreign_key, :target_name, :target_model, :to => :metadata
53
+ delegate :primary_key, :foreign_key, :target_name, :target_model, :base_criteria, :to => :metadata
53
54
 
54
55
  def initialize(metadata, owner)
55
56
  @metadata, @owner = metadata, owner