nobrainer 0.22.0 → 0.28.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +1 -1
  3. data/lib/no_brainer/config.rb +54 -14
  4. data/lib/no_brainer/connection.rb +24 -21
  5. data/lib/no_brainer/connection_manager.rb +29 -9
  6. data/lib/no_brainer/criteria/cache.rb +8 -0
  7. data/lib/no_brainer/criteria/changes.rb +16 -0
  8. data/lib/no_brainer/criteria/core.rb +4 -5
  9. data/lib/no_brainer/criteria/eager_load.rb +2 -2
  10. data/lib/no_brainer/criteria/enumerable.rb +3 -1
  11. data/lib/no_brainer/criteria/find.rb +6 -9
  12. data/lib/no_brainer/criteria/first.rb +3 -3
  13. data/lib/no_brainer/criteria/first_or_create.rb +114 -0
  14. data/lib/no_brainer/criteria/join.rb +62 -0
  15. data/lib/no_brainer/criteria/order_by.rb +19 -16
  16. data/lib/no_brainer/criteria/run.rb +26 -0
  17. data/lib/no_brainer/criteria/scope.rb +8 -8
  18. data/lib/no_brainer/criteria/where.rb +130 -107
  19. data/lib/no_brainer/criteria.rb +4 -4
  20. data/lib/no_brainer/document/association/belongs_to.rb +44 -17
  21. data/lib/no_brainer/document/association/core.rb +11 -0
  22. data/lib/no_brainer/document/association/eager_loader.rb +26 -26
  23. data/lib/no_brainer/document/association/has_many.rb +7 -9
  24. data/lib/no_brainer/document/association/has_many_through.rb +1 -2
  25. data/lib/no_brainer/document/association/has_one.rb +5 -1
  26. data/lib/no_brainer/document/association.rb +5 -5
  27. data/lib/no_brainer/document/atomic_ops.rb +40 -7
  28. data/lib/no_brainer/document/attributes.rb +24 -15
  29. data/lib/no_brainer/document/callbacks.rb +1 -1
  30. data/lib/no_brainer/document/core.rb +18 -18
  31. data/lib/no_brainer/document/criteria.rb +9 -5
  32. data/lib/no_brainer/document/dirty.rb +15 -12
  33. data/lib/no_brainer/document/dynamic_attributes.rb +6 -0
  34. data/lib/no_brainer/document/index/index.rb +5 -9
  35. data/lib/no_brainer/document/index/meta_store.rb +1 -10
  36. data/lib/no_brainer/document/index/synchronizer.rb +16 -20
  37. data/lib/no_brainer/document/index.rb +3 -3
  38. data/lib/no_brainer/document/lazy_fetch.rb +3 -3
  39. data/lib/no_brainer/document/missing_attributes.rb +7 -2
  40. data/lib/no_brainer/document/persistance.rb +14 -30
  41. data/lib/no_brainer/document/polymorphic.rb +8 -4
  42. data/lib/no_brainer/document/primary_key/generator.rb +6 -1
  43. data/lib/no_brainer/document/primary_key.rb +19 -4
  44. data/lib/no_brainer/document/serialization.rb +0 -2
  45. data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
  46. data/lib/no_brainer/document/table_config.rb +118 -0
  47. data/lib/no_brainer/document/timestamps.rb +8 -0
  48. data/lib/no_brainer/document/validation/core.rb +63 -0
  49. data/lib/no_brainer/document/validation/uniqueness.rb +30 -36
  50. data/lib/no_brainer/document/validation.rb +1 -58
  51. data/lib/no_brainer/document.rb +2 -2
  52. data/lib/no_brainer/error.rb +1 -0
  53. data/lib/no_brainer/geo/base.rb +0 -1
  54. data/lib/no_brainer/locale/en.yml +1 -0
  55. data/lib/no_brainer/lock.rb +12 -8
  56. data/lib/no_brainer/profiler/controller_runtime.rb +76 -0
  57. data/lib/no_brainer/{query_runner → profiler}/logger.rb +11 -27
  58. data/lib/no_brainer/profiler.rb +11 -0
  59. data/lib/no_brainer/query_runner/database_on_demand.rb +8 -8
  60. data/lib/no_brainer/query_runner/driver.rb +3 -1
  61. data/lib/no_brainer/query_runner/missing_index.rb +3 -3
  62. data/lib/no_brainer/query_runner/profiler.rb +43 -0
  63. data/lib/no_brainer/query_runner/reconnect.rb +38 -23
  64. data/lib/no_brainer/query_runner/run_options.rb +35 -15
  65. data/lib/no_brainer/query_runner/table_on_demand.rb +18 -11
  66. data/lib/no_brainer/query_runner.rb +3 -3
  67. data/lib/no_brainer/railtie/database.rake +14 -4
  68. data/lib/no_brainer/railtie.rb +5 -12
  69. data/lib/no_brainer/rql.rb +11 -8
  70. data/lib/no_brainer/symbol_decoration.rb +11 -0
  71. data/lib/no_brainer/system/cluster_config.rb +5 -0
  72. data/lib/no_brainer/system/db_config.rb +5 -0
  73. data/lib/no_brainer/system/document.rb +24 -0
  74. data/lib/no_brainer/system/issue.rb +10 -0
  75. data/lib/no_brainer/system/job.rb +10 -0
  76. data/lib/no_brainer/system/log.rb +11 -0
  77. data/lib/no_brainer/system/server_config.rb +7 -0
  78. data/lib/no_brainer/system/server_status.rb +9 -0
  79. data/lib/no_brainer/system/stat.rb +11 -0
  80. data/lib/no_brainer/system/table_config.rb +10 -0
  81. data/lib/no_brainer/system/table_status.rb +8 -0
  82. data/lib/no_brainer/system.rb +17 -0
  83. data/lib/nobrainer.rb +16 -11
  84. data/lib/rails/generators/nobrainer/install_generator.rb +48 -0
  85. data/lib/rails/generators/nobrainer/{model/model_generator.rb → model_generator.rb} +7 -3
  86. data/lib/rails/generators/nobrainer/namespace_fix.rb +15 -0
  87. data/lib/rails/generators/{nobrainer/model/templates/model.rb.tt → templates/model.rb} +1 -1
  88. data/lib/rails/generators/templates/nobrainer.rb +101 -0
  89. metadata +34 -10
  90. data/lib/no_brainer/document/store_in.rb +0 -35
  91. data/lib/rails/generators/nobrainer.rb +0 -18
@@ -2,7 +2,8 @@ module NoBrainer::Criteria::OrderBy
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  # The latest order_by() wins
5
- included { criteria_option :order_by, :ordering_mode, :merge_with => :set_scalar }
5
+ included { criteria_option :order_by, :ordering_mode, :reversed_ordering,
6
+ :merge_with => :set_scalar }
6
7
 
7
8
  def order_by(*rules, &block)
8
9
  # Note: We are relying on the fact that Hashes are ordered (since 1.9)
@@ -17,7 +18,10 @@ module NoBrainer::Criteria::OrderBy
17
18
  end
18
19
  end.reduce({}, :merge)
19
20
 
20
- chain(:order_by => rules, :ordering_mode => :normal)
21
+ rules.keys.each { |k| model.ensure_valid_key!(k) unless k.is_a?(Proc) } if model
22
+
23
+ chain(:order_by => rules, :ordering_mode => :explicit,
24
+ :reversed_ordering => false)
21
25
  end
22
26
 
23
27
  def without_ordering
@@ -25,12 +29,7 @@ module NoBrainer::Criteria::OrderBy
25
29
  end
26
30
 
27
31
  def reverse_order
28
- chain(:ordering_mode => case @options[:ordering_mode]
29
- when nil then :reversed
30
- when :normal then :reversed
31
- when :reversed then :normal
32
- when :disabled then :disabled
33
- end)
32
+ chain(:reversed_ordering => !@options[:reversed_ordering])
34
33
  end
35
34
 
36
35
  def order_by_indexed?
@@ -43,16 +42,21 @@ module NoBrainer::Criteria::OrderBy
43
42
 
44
43
  private
45
44
 
46
- def effective_order
47
- @options[:order_by].presence || (model ? {model.pk_name => :asc} : {})
45
+ def ordering_mode
46
+ @options[:ordering_mode] || :implicit
48
47
  end
49
48
 
50
49
  def reverse_order?
51
- @options[:ordering_mode] == :reversed
50
+ !!@options[:reversed_ordering]
52
51
  end
53
52
 
54
- def should_order?
55
- @options[:ordering_mode] != :disabled
53
+ def effective_order
54
+ # reversing the order happens later.
55
+ case ordering_mode
56
+ when :disabled then nil
57
+ when :explicit then @options[:order_by]
58
+ when :implicit then model && {model.pk_name => :asc}
59
+ end
56
60
  end
57
61
 
58
62
  class IndexFinder < Struct.new(:criteria, :index_name, :rql_proc)
@@ -61,7 +65,7 @@ module NoBrainer::Criteria::OrderBy
61
65
  end
62
66
 
63
67
  def first_key
64
- @first_key ||= criteria.__send__(:effective_order).first.try(:[], 0)
68
+ @first_key ||= criteria.__send__(:effective_order).to_a.first.try(:[], 0)
65
69
  end
66
70
 
67
71
  def first_key_indexable?
@@ -94,9 +98,8 @@ module NoBrainer::Criteria::OrderBy
94
98
 
95
99
  def compile_rql_pass1
96
100
  rql = super
97
- return rql unless should_order?
98
101
  _effective_order = effective_order
99
- return rql if _effective_order.empty?
102
+ return rql unless _effective_order.present?
100
103
 
101
104
  rql_rules = _effective_order.map do |k,v|
102
105
  if order_by_index_finder.index_name == k
@@ -0,0 +1,26 @@
1
+ module NoBrainer::Criteria::Run
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ criteria_option :initial_run_options, :merge_with => :set_scalar
6
+ criteria_option :run_with, :merge_with => :merge_hash
7
+ end
8
+
9
+ private
10
+
11
+ def run(&block)
12
+ return finalized_criteria.__send__(:run, &block) unless finalized?
13
+ ensure_same_run_option_context!
14
+
15
+ block ||= proc { to_rql }
16
+ NoBrainer.run(:criteria => self, &block)
17
+ end
18
+
19
+ def ensure_same_run_option_context!
20
+ return if @options[:initial_run_options].nil?
21
+ return if @options[:initial_run_options] == NoBrainer.current_run_options
22
+
23
+ raise "The current criteria cannot be executed as it was constructed in a different `run_with()' context\n" +
24
+ "Note: you may use `run_with()' directly in your query (e.g. Model.run_with(...).first)."
25
+ end
26
+ end
@@ -3,10 +3,6 @@ module NoBrainer::Criteria::Scope
3
3
 
4
4
  included { criteria_option :use_default_scope, :merge_with => :set_scalar }
5
5
 
6
- def scoped
7
- chain(:use_default_scope => true)
8
- end
9
-
10
6
  def unscoped
11
7
  chain(:use_default_scope => false)
12
8
  end
@@ -17,21 +13,25 @@ module NoBrainer::Criteria::Scope
17
13
 
18
14
  def method_missing(name, *args, &block)
19
15
  return super unless self.model.respond_to?(name)
16
+ apply_named_scope(name, args, block)
17
+ end
18
+
19
+ private
20
+
21
+ def apply_named_scope(name, args, block)
20
22
  criteria = self.model.method(name).call(*args, &block)
21
23
  raise "#{name} did not return a criteria" unless criteria.is_a?(NoBrainer::Criteria)
22
24
  merge(criteria)
23
25
  end
24
26
 
25
- private
26
-
27
- def _apply_default_scope
27
+ def apply_default_scope
28
28
  return self if @options[:use_default_scope] == false
29
29
  (model.default_scopes.map(&:call).compact + [self]).reduce(:merge)
30
30
  end
31
31
 
32
32
  module ClassMethods
33
33
  def _finalize_criteria(base)
34
- super.__send__(:_apply_default_scope)
34
+ super.__send__(:apply_default_scope)
35
35
  end
36
36
  end
37
37
  end
@@ -1,12 +1,4 @@
1
1
  module NoBrainer::Criteria::Where
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)
9
-
10
2
  extend ActiveSupport::Concern
11
3
  include ActiveModel::ForbiddenAttributesProtection
12
4
 
@@ -19,6 +11,10 @@ module NoBrainer::Criteria::Where
19
11
  chain(:where_ast => parse_clause([*args, block].compact))
20
12
  end
21
13
 
14
+ def _where(*args, &block)
15
+ chain(:where_ast => parse_clause([*args, block].compact, :unsafe => true))
16
+ end
17
+
22
18
  def self.merge_where_ast(a, b)
23
19
  (a ? MultiOperator.new(:and, [a, b]) : b).simplify
24
20
  end
@@ -57,7 +53,7 @@ module NoBrainer::Criteria::Where
57
53
  same_op_clauses, other_clauses = clauses.partition do |v|
58
54
  v.is_a?(self.class) && (v.clauses.size == 1 || v.op == self.op)
59
55
  end
60
- simplified_clauses = other_clauses + same_op_clauses.map(&:clauses).flatten(1)
56
+ simplified_clauses = other_clauses + same_op_clauses.flat_map(&:clauses)
61
57
  simplified_clauses = BinaryOperator.simplify_clauses(op, simplified_clauses.uniq)
62
58
  self.class.new(op, simplified_clauses)
63
59
  end
@@ -70,7 +66,7 @@ module NoBrainer::Criteria::Where
70
66
  end
71
67
  end
72
68
 
73
- class BinaryOperator < Struct.new(:key, :key_modifier, :op, :value, :model, :casted_values)
69
+ class BinaryOperator < Struct.new(:key_path, :key_modifier, :op, :value, :model, :casted_values)
74
70
  def self.get_candidate_clauses(clauses, *types)
75
71
  clauses.select { |c| c.is_a?(self) && types.include?(c.op) }
76
72
  end
@@ -79,10 +75,10 @@ module NoBrainer::Criteria::Where
79
75
  # This code assumes that simplfy() has already been called on all clauses.
80
76
  if op == :or
81
77
  eq_clauses = get_candidate_clauses(ast_clauses, :in, :eq)
82
- new_clauses = eq_clauses.group_by { |c| [c.key, c.key_modifier] }.map do |(key, key_modifier), clauses|
78
+ new_clauses = eq_clauses.group_by { |c| [c.key_path, c.key_modifier] }.map do |(key_path, key_modifier), clauses|
83
79
  if key_modifier.in?([:scalar, :any]) && clauses.size > 1
84
- values = clauses.map { |c| c.op == :in ? c.value : [c.value] }.flatten(1).uniq
85
- [BinaryOperator.new(key, key_modifier, :in, values, clauses.first.model, true)]
80
+ values = clauses.flat_map { |c| c.op == :in ? c.value : [c.value] }.uniq
81
+ [BinaryOperator.new(key_path, key_modifier, :in, values, clauses.first.model, true)]
86
82
  else
87
83
  clauses
88
84
  end
@@ -97,30 +93,33 @@ module NoBrainer::Criteria::Where
97
93
  end
98
94
 
99
95
  def simplify
100
- new_key = cast_key(key)
101
- new_op, new_value = case op
96
+ new_key_path = cast_key_path(key_path)
97
+ new_key_modifier, new_op, new_value = case op
102
98
  when :in then
103
99
  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]
100
+ when Range then [key_modifier, :between, (cast_value(value.min)..cast_value(value.max))]
101
+ when Array then [key_modifier, :in, value.map(&method(:cast_value)).uniq]
106
102
  else raise ArgumentError.new "`in' takes an array/range, not #{value}"
107
103
  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)]
104
+ when :between then [key_modifier, op, (cast_value(value.min)..cast_value(value.max))]
105
+ when :include then ensure_scalar_for(op); [:any, :eq, cast_value(value)]
106
+ when :defined, :undefined then ensure_scalar_for(op); [key_modifier, op, cast_value(value)]
107
+ else [key_modifier, op, cast_value(value)]
113
108
  end
114
- BinaryOperator.new(new_key, key_modifier, new_op, new_value, model, true)
109
+ BinaryOperator.new(new_key_path, new_key_modifier, new_op, new_value, model, true)
115
110
  end
116
111
 
117
112
  def to_rql(doc)
118
- key = model.lookup_field_alias(self.key)
113
+ key_path = [model.lookup_field_alias(self.key_path.first), *self.key_path[1..-1]]
114
+
115
+ doc = key_path[0..-2].reduce(doc) { |d,k| d[k] }
116
+ key = key_path.last
119
117
 
120
118
  case key_modifier
121
119
  when :scalar then
122
120
  case op
123
- when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
121
+ when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
122
+ when :undefined then !value ? doc.has_fields(key) : doc.has_fields(key).not
124
123
  else to_rql_scalar(doc[key])
125
124
  end
126
125
  when :any then doc[key].map { |lvalue| to_rql_scalar(lvalue) }.contains(true)
@@ -134,12 +133,9 @@ module NoBrainer::Criteria::Where
134
133
  when :in then RethinkDB::RQL.new.expr(value).contains(lvalue)
135
134
  when :intersects then lvalue.intersects(value.to_rql)
136
135
  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
136
+ # XXX options[:max_results] is not used, seems to be a workaround of rethinkdb index implementation.
137
+ circle = value[:circle]
138
+ RethinkDB::RQL.new.distance(lvalue, circle.center.to_rql, circle.options) <= circle.radius
143
139
  else lvalue.__send__(op, value)
144
140
  end
145
141
  end
@@ -150,11 +146,13 @@ module NoBrainer::Criteria::Where
150
146
 
151
147
  private
152
148
 
149
+ def ensure_scalar_for(op)
150
+ raise "Incorrect use of `#{op}' and `#{key_modifier}'" if key_modifier != :scalar
151
+ end
152
+
153
153
  def association
154
- # FIXME This leaks memory with dynamic attributes. The internals of type
155
- # checking will convert the key to a symbol, and Ruby does not garbage
156
- # collect symbols.
157
- @association ||= [model.association_metadata[key.to_sym]]
154
+ return nil if key_path.size > 1
155
+ @association ||= [model.association_metadata[key_path.first]]
158
156
  @association.first
159
157
  end
160
158
 
@@ -165,61 +163,75 @@ module NoBrainer::Criteria::Where
165
163
  when NoBrainer::Document::Association::BelongsTo::Metadata
166
164
  target_model = association.target_model
167
165
  unless value.is_a?(target_model)
168
- opts = { :model => model, :attr_name => key, :type => target_model, :value => value }
166
+ opts = { :model => model, :attr_name => key_path.first, :type => target_model, :value => value }
169
167
  raise NoBrainer::Error::InvalidType.new(opts)
170
168
  end
171
169
  value.pk_value
172
170
  else
173
171
  case op
174
- when :defined then NoBrainer::Boolean.nobrainer_cast_user_to_model(value)
172
+ when :defined, :undefined then NoBrainer::Boolean.nobrainer_cast_user_to_model(value)
175
173
  when :intersects
176
174
  raise "Use a geo object with `intersects`" unless value.is_a?(NoBrainer::Geo::Base)
177
175
  value
178
176
  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])
177
+ # TODO enforce key is a geo type
178
+ case value
179
+ when Hash
180
+ options = NoBrainer::Geo::Base.normalize_geo_options(value)
181
+
182
+ options[:radius] = options.delete(:max_distance) if options[:max_distance]
183
+ options[:radius] = options.delete(:max_dist) if options[:max_dist]
184
+ options[:center] = options.delete(:point) if options[:point]
185
+
186
+ unless options[:circle]
187
+ unless options[:center] && options[:radius]
188
+ raise "`near' takes something like {:center => P, :radius => d}"
189
+ end
190
+ { :circle => NoBrainer::Geo::Circle.new(options), :max_results => options[:max_results] }
191
+ end
192
+ when NoBrainer::Geo::Circle then { :circle => value }
193
+ else raise "Incorrect use of `near': rvalue must be a hash or a circle"
188
194
  end
189
-
190
- options
191
195
  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
+ # 1) Box value in array if we have an any/all modifier
197
+ # 2) Box value in hash if we have a nested query.
198
+ value = [value] if key_modifier.in?([:any, :all])
199
+ value_hash = key_path.reverse.reduce(value) { |v,k| {k => v} }
200
+ value = model.cast_user_to_db_for(*value_hash.first)
201
+ value = key_path[1..-1].reduce(value) { |h,k| h[k] }
202
+ value = value.first if key_modifier.in?([:any, :all])
203
+ value
196
204
  end
197
205
  end
198
206
  end
199
207
 
200
- def cast_key(key)
208
+ def cast_key_path(key)
201
209
  return key if casted_values
202
210
 
203
211
  case association
204
- when NoBrainer::Document::Association::BelongsTo::Metadata then association.foreign_key
205
- else ensure_valid_key!(key); key
212
+ when NoBrainer::Document::Association::BelongsTo::Metadata then [association.foreign_key]
213
+ else model.ensure_valid_key!(key_path.first); key_path
206
214
  end
207
215
  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
213
216
  end
214
217
 
215
- class UnaryOperator < Struct.new(:op, :value)
218
+ class UnaryOperator < Struct.new(:op, :clause)
216
219
  def simplify
217
- value.is_a?(UnaryOperator) && [self.op, value.op] == [:not, :not] ? value.value : self
220
+ simplified_clause = self.clause.simplify
221
+
222
+ case simplified_clause
223
+ when UnaryOperator then
224
+ case [self.op, simplified_clause.op]
225
+ when [:not, :not] then simplified_clause.clause
226
+ else self.class.new(op, simplified_clause)
227
+ end
228
+ else self.class.new(op, simplified_clause)
229
+ end
218
230
  end
219
231
 
220
232
  def to_rql(doc)
221
233
  case op
222
- when :not then value.to_rql(doc).not
234
+ when :not then clause.to_rql(doc).not
223
235
  end
224
236
  end
225
237
  end
@@ -234,60 +246,53 @@ module NoBrainer::Criteria::Where
234
246
  end
235
247
  end
236
248
 
237
- def parse_clause(clause)
249
+ def parse_clause(clause, options={})
238
250
  clause = sanitize_for_mass_assignment(clause)
239
251
  case clause
240
- when Array then MultiOperator.new(:and, clause.map { |c| parse_clause(c) })
241
- when Hash then MultiOperator.new(:and, clause.map { |k,v| parse_clause_stub(k,v) })
252
+ when Array then MultiOperator.new(:and, clause.map { |c| parse_clause(c, options) })
253
+ when Hash then MultiOperator.new(:and, clause.map { |k,v| parse_clause_stub(k, v, options) })
242
254
  when Proc then Lambda.new(clause)
243
255
  when Symbol::Decoration
244
256
  case clause.args.size
245
- when 1 then parse_clause_stub(clause, clause.args.first)
257
+ when 1 then parse_clause_stub(clause, clause.args.first, options)
246
258
  else raise "Invalid argument: #{clause}"
247
259
  end
248
260
  else raise "Invalid clause: #{clause}"
249
261
  end
250
262
  end
251
263
 
252
- def parse_clause_stub(key, value)
264
+ def parse_clause_stub(key, value, options={})
253
265
  case key
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))
259
- when String, Symbol then parse_clause_stub_eq(key, value)
266
+ when :and then parse_multi_value(:and, value, false, options)
267
+ when :or then parse_multi_value(:or, value, false, options)
268
+ when :_and then parse_multi_value(:and, value, true, options)
269
+ when :_or then parse_multi_value(:or, value, true, options)
270
+ when :not then UnaryOperator.new(:not, parse_clause(value, options))
271
+ when String, Symbol then
272
+ case value
273
+ when Hash then parse_clause(value, options.merge(:nested_prefix => (options[:nested_prefix] || []) + [key.to_sym]))
274
+ else instantiate_binary_op(key.to_sym, :eq, value, options)
275
+ end
260
276
  when Symbol::Decoration then
261
277
  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 })
264
- when :nin then parse_clause(:not => { key.symbol.in => value })
265
- when :gte then parse_clause(key.symbol.ge => value)
266
- when :lte then parse_clause(key.symbol.le => value)
267
- when :eq then parse_clause_stub_eq(key.symbol, value)
268
- else instantiate_binary_op(key.symbol, key.decorator, value)
278
+ when :any, :all, :not then instantiate_binary_op(key, :eq, value, options)
279
+ when :gte then instantiate_binary_op(key.symbol, :ge, value, options)
280
+ when :lte then instantiate_binary_op(key.symbol, :le, value, options)
281
+ else instantiate_binary_op(key.symbol, key.decorator, value, options)
269
282
  end
270
283
  else raise "Invalid key: #{key}"
271
284
  end
272
285
  end
273
286
 
274
- def parse_multi_value(op, value, options={})
287
+ def parse_multi_value(op, value, multi_safe, options={})
275
288
  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]
289
+ if value.size == 1 && value.first.is_a?(Hash) && !multi_safe
277
290
  raise "The `#{op}' operator was provided an array with a single hash element.\n" +
278
291
  "In Ruby, [:a => :b, :c => :d] means [{:a => :b, :c => :d}] which is not the same as [{:a => :b}, {:c => :d}].\n" +
279
292
  "To prevent mistakes, the former construct is prohibited as you probably mean the latter.\n" +
280
293
  "However, if you know what you are doing, you can use the `_#{op}' operator instead."
281
294
  end
282
- MultiOperator.new(op, value.map { |v| parse_clause(v) })
283
- end
284
-
285
- def parse_clause_stub_eq(key, value)
286
- case value
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)
290
- end
295
+ MultiOperator.new(op, value.map { |v| parse_clause(v, options) })
291
296
  end
292
297
 
293
298
  def translate_regexp_to_re2_syntax(value)
@@ -303,10 +308,25 @@ module NoBrainer::Criteria::Where
303
308
  "(?#{flags})#{value.source}"
304
309
  end
305
310
 
306
- def instantiate_binary_op(key, op, value)
311
+ def instantiate_binary_op(key, op, value, options={})
312
+ op, value = case value
313
+ when Range then [:between, value]
314
+ when Regexp then [:match, translate_regexp_to_re2_syntax(value)]
315
+ else [:eq, value]
316
+ end if op == :eq
317
+
318
+ nested_prefix = options[:nested_prefix] || []
319
+
320
+ tail_args = [op, value, self.model, !!options[:unsafe]]
321
+
307
322
  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)
323
+ when Symbol::Decoration
324
+ raise "Use only one .not, .all or .any modifiers in the query" if key.symbol.is_a?(Symbol::Decoration)
325
+ case key.decorator
326
+ when :any, :all then BinaryOperator.new(nested_prefix + [key.symbol], key.decorator, *tail_args)
327
+ when :not then UnaryOperator.new(:not, BinaryOperator.new(nested_prefix + [key.symbol], :scalar, *tail_args))
328
+ end
329
+ else BinaryOperator.new(nested_prefix + [key], :scalar, *tail_args)
310
330
  end
311
331
  end
312
332
 
@@ -319,12 +339,12 @@ module NoBrainer::Criteria::Where
319
339
 
320
340
  def rql_proc
321
341
  lambda do |rql|
342
+ return RethinkDB::RQL.new.expr([]) if rql_op == :get_all && rql_args.empty?
343
+
322
344
  opt = (rql_options || {}).merge(:index => index.aliased_name)
323
345
  r = rql.__send__(rql_op, *rql_args, opt)
324
346
  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]
347
+ r = r.distinct if index.multi && !index_finder.criteria.options[:without_distinct]
328
348
  r
329
349
  end
330
350
  end
@@ -347,9 +367,9 @@ module NoBrainer::Criteria::Where
347
367
  clauses = get_candidate_clauses(:eq, :in, :between, :near, :intersects)
348
368
  return nil unless clauses.present?
349
369
 
350
- usable_indexes = Hash[get_usable_indexes.map { |i| [i.name, i] }]
370
+ usable_indexes = Hash[get_usable_indexes.map { |i| [[i.name], i] }]
351
371
  clauses.map do |clause|
352
- index = usable_indexes[clause.key]
372
+ index = usable_indexes[clause.key_path]
353
373
  next unless index && clause.compatible_with_index?(index)
354
374
  next unless index.geo == [:near, :intersects].include?(clause.op)
355
375
 
@@ -357,8 +377,10 @@ module NoBrainer::Criteria::Where
357
377
  when :intersects then [:get_intersecting, clause.value.to_rql]
358
378
  when :near
359
379
  options = clause.value.dup
360
- point = options.delete(:point)
361
- [:get_nearest, point.to_rql, options]
380
+ circle = options.delete(:circle)
381
+ options.delete(:max_results) if options[:max_results].nil?
382
+ options[:max_dist] = circle.radius
383
+ [:get_nearest, circle.center.to_rql, circle.options.merge(options)]
362
384
  when :eq then [:get_all, [clause.value]]
363
385
  when :in then [:get_all, clause.value]
364
386
  when :between then [:between, [clause.value.min, clause.value.max],
@@ -369,11 +391,11 @@ module NoBrainer::Criteria::Where
369
391
  end
370
392
 
371
393
  def find_strategy_compound
372
- clauses = Hash[get_candidate_clauses(:eq).map { |c| [c.key, c] }]
394
+ clauses = Hash[get_candidate_clauses(:eq).map { |c| [c.key_path, c] }]
373
395
  return nil unless clauses.present?
374
396
 
375
397
  get_usable_indexes(:kind => :compound, :geo => false, :multi => false).each do |index|
376
- indexed_clauses = index.what.map { |field| clauses[field] }
398
+ indexed_clauses = index.what.map { |field| clauses[[field]] }
377
399
  next unless indexed_clauses.all? { |c| c.try(:compatible_with_index?, index) }
378
400
 
379
401
  return IndexStrategy.new(self, ast, indexed_clauses, index, :get_all, [indexed_clauses.map(&:value)])
@@ -382,11 +404,11 @@ module NoBrainer::Criteria::Where
382
404
  end
383
405
 
384
406
  def find_strategy_hidden_between
385
- clauses = get_candidate_clauses(:gt, :ge, :lt, :le).group_by(&:key)
407
+ clauses = get_candidate_clauses(:gt, :ge, :lt, :le).group_by(&:key_path)
386
408
  return nil unless clauses.present?
387
409
 
388
410
  get_usable_indexes(:geo => false).each do |index|
389
- matched_clauses = clauses[index.name].try(:select) { |c| c.compatible_with_index?(index) }
411
+ matched_clauses = clauses[[index.name]].try(:select) { |c| c.compatible_with_index?(index) }
390
412
  next unless matched_clauses.present?
391
413
 
392
414
  op_clauses = Hash[matched_clauses.map { |c| [c.op, c] }]
@@ -401,7 +423,8 @@ module NoBrainer::Criteria::Where
401
423
  options[:right_bound] = {:lt => :open, :le => :closed}[right_bound.op] if right_bound
402
424
 
403
425
  return IndexStrategy.new(self, ast, [left_bound, right_bound].compact, index, :between,
404
- [left_bound.try(:value), right_bound.try(:value)], options)
426
+ [left_bound ? left_bound.try(:value) : RethinkDB::RQL.new.minval,
427
+ right_bound ? right_bound.try(:value) : RethinkDB::RQL.new.maxval], options)
405
428
  end
406
429
  return nil
407
430
  end
@@ -2,8 +2,8 @@ require 'rethinkdb'
2
2
 
3
3
  class NoBrainer::Criteria
4
4
  extend NoBrainer::Autoload
5
- autoload_and_include :Core, :Raw, :AfterFind, :Where, :OrderBy, :Limit,
6
- :Pluck, :Count, :Delete, :Enumerable, :First, :Find,
7
- :Aggregate, :EagerLoad, :Update, :Cache, :Index,
8
- :Extend, :Scope
5
+ autoload_and_include :Core, :Run, :Raw, :Scope, :AfterFind, :Where, :OrderBy,
6
+ :Limit, :Pluck, :Count, :Delete, :Enumerable, :Find,
7
+ :First, :FirstOrCreate, :Changes, :Aggregate, :EagerLoad,
8
+ :Update, :Cache, :Index, :Extend, :Join
9
9
  end