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.
- checksums.yaml +4 -4
- data/lib/no_brainer/autoload.rb +0 -5
- data/lib/no_brainer/config.rb +73 -39
- data/lib/no_brainer/connection.rb +2 -4
- data/lib/no_brainer/criteria/after_find.rb +3 -11
- data/lib/no_brainer/criteria/aggregate.rb +2 -2
- data/lib/no_brainer/criteria/cache.rb +15 -10
- data/lib/no_brainer/criteria/core.rb +46 -11
- data/lib/no_brainer/criteria/delete.rb +2 -2
- data/lib/no_brainer/criteria/eager_load.rb +51 -0
- data/lib/no_brainer/criteria/extend.rb +4 -16
- data/lib/no_brainer/criteria/find.rb +27 -0
- data/lib/no_brainer/criteria/index.rb +7 -13
- data/lib/no_brainer/criteria/limit.rb +5 -12
- data/lib/no_brainer/criteria/order_by.rb +20 -36
- data/lib/no_brainer/criteria/pluck.rb +16 -22
- data/lib/no_brainer/criteria/raw.rb +4 -10
- data/lib/no_brainer/criteria/scope.rb +6 -19
- data/lib/no_brainer/criteria/update.rb +8 -6
- data/lib/no_brainer/criteria/where.rb +252 -138
- data/lib/no_brainer/criteria.rb +3 -2
- data/lib/no_brainer/document/aliases.rb +3 -3
- data/lib/no_brainer/document/association/belongs_to.rb +9 -5
- data/lib/no_brainer/document/association/core.rb +6 -5
- data/lib/no_brainer/document/association/eager_loader.rb +9 -9
- data/lib/no_brainer/document/association/has_many.rb +23 -9
- data/lib/no_brainer/document/association/has_many_through.rb +12 -3
- data/lib/no_brainer/document/atomic_ops.rb +79 -78
- data/lib/no_brainer/document/attributes.rb +24 -20
- data/lib/no_brainer/document/callbacks.rb +1 -1
- data/lib/no_brainer/document/core.rb +5 -2
- data/lib/no_brainer/document/criteria.rb +14 -19
- data/lib/no_brainer/document/dirty.rb +11 -16
- data/lib/no_brainer/document/index/index.rb +2 -1
- data/lib/no_brainer/document/index/meta_store.rb +1 -1
- data/lib/no_brainer/document/index.rb +14 -10
- data/lib/no_brainer/document/persistance.rb +24 -13
- data/lib/no_brainer/document/primary_key/generator.rb +83 -0
- data/lib/no_brainer/document/{id.rb → primary_key.rb} +9 -36
- data/lib/no_brainer/document/store_in.rb +2 -2
- data/lib/no_brainer/document/timestamps.rb +4 -2
- data/lib/no_brainer/document/types/binary.rb +2 -7
- data/lib/no_brainer/document/types/boolean.rb +2 -4
- data/lib/no_brainer/document/types/date.rb +2 -2
- data/lib/no_brainer/document/types/float.rb +2 -2
- data/lib/no_brainer/document/types/geo.rb +1 -0
- data/lib/no_brainer/document/types/integer.rb +2 -2
- data/lib/no_brainer/document/types/set.rb +2 -2
- data/lib/no_brainer/document/types/string.rb +5 -2
- data/lib/no_brainer/document/types/symbol.rb +2 -2
- data/lib/no_brainer/document/types/text.rb +18 -0
- data/lib/no_brainer/document/types/time.rb +2 -2
- data/lib/no_brainer/document/types.rb +17 -18
- data/lib/no_brainer/document/validation/not_null.rb +15 -0
- data/lib/no_brainer/document/{uniqueness.rb → validation/uniqueness.rb} +11 -11
- data/lib/no_brainer/document/validation.rb +35 -6
- data/lib/no_brainer/document.rb +1 -1
- data/lib/no_brainer/error.rb +21 -19
- data/lib/no_brainer/geo/base.rb +16 -0
- data/lib/no_brainer/geo/circle.rb +25 -0
- data/lib/no_brainer/geo/line_string.rb +11 -0
- data/lib/no_brainer/geo/point.rb +49 -0
- data/lib/no_brainer/geo/polygon.rb +11 -0
- data/lib/no_brainer/geo.rb +4 -0
- data/lib/no_brainer/locale/en.yml +1 -0
- data/lib/no_brainer/lock.rb +114 -0
- data/lib/no_brainer/query_runner/connection_lock.rb +1 -1
- data/lib/no_brainer/query_runner/database_on_demand.rb +0 -1
- data/lib/no_brainer/query_runner/missing_index.rb +1 -1
- data/lib/no_brainer/query_runner/reconnect.rb +9 -11
- data/lib/no_brainer/query_runner/run_options.rb +0 -3
- data/lib/no_brainer/query_runner/table_on_demand.rb +3 -4
- data/lib/no_brainer/railtie/database.rake +2 -2
- data/lib/no_brainer/rql.rb +1 -5
- data/lib/nobrainer.rb +2 -6
- data/lib/rails/generators/nobrainer.rb +1 -1
- metadata +34 -9
- data/lib/no_brainer/criteria/preload.rb +0 -50
- data/lib/no_brainer/decorated_symbol.rb +0 -17
|
@@ -1,47 +1,48 @@
|
|
|
1
1
|
module NoBrainer::Criteria::Where
|
|
2
|
-
|
|
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
|
-
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
include ActiveModel::ForbiddenAttributesProtection
|
|
5
12
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
19
|
+
chain(:where_ast => parse_clause([*args, block].compact))
|
|
12
20
|
end
|
|
13
21
|
|
|
14
|
-
def
|
|
15
|
-
|
|
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
|
-
|
|
31
|
+
where_index_name.present?
|
|
37
32
|
end
|
|
38
33
|
|
|
39
34
|
def where_index_name
|
|
40
|
-
where_index_finder.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
67
|
-
when :or then clauses.map { |c| c.to_rql(doc) }.reduce
|
|
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
|
|
82
|
-
|
|
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
|
-
|
|
100
|
-
case op
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 :
|
|
116
|
-
when :
|
|
117
|
-
when :
|
|
118
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
195
|
-
when :or
|
|
196
|
-
when :
|
|
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
|
|
199
|
-
case key.
|
|
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 :
|
|
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
|
|
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
|
|
212
|
-
when Regexp then
|
|
213
|
-
else
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
indexes = indexes.select { |i|
|
|
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
|
|
243
|
-
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
when :
|
|
259
|
-
when :
|
|
260
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
375
|
+
get_usable_indexes(:kind => :compound, :geo => false, :multi => false).each do |index|
|
|
270
376
|
indexed_clauses = index.what.map { |field| clauses[field] }
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
self
|
|
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
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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[:
|
|
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
|
-
|
|
295
|
-
self
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
320
|
-
return
|
|
429
|
+
def find_strategy
|
|
430
|
+
return nil unless ast.try(:clauses).present? && !criteria.without_index?
|
|
321
431
|
case ast.op
|
|
322
|
-
when :and then
|
|
323
|
-
when :or then
|
|
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
|
|
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
|
|
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 =
|
|
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
|
data/lib/no_brainer/criteria.rb
CHANGED
|
@@ -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, :
|
|
7
|
-
:
|
|
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[:
|
|
16
|
-
self.alias_map[attr.to_s] = options[:
|
|
17
|
-
self.alias_reverse_map[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
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, :
|
|
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, :
|
|
33
|
-
owner_model.validates(target_name,
|
|
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(
|
|
23
|
+
def delegate(method_src, method_dst, options={})
|
|
24
24
|
metadata = self
|
|
25
25
|
owner_model.inject_in_layer :associations do
|
|
26
|
-
define_method(
|
|
26
|
+
define_method(method_src) do |*args, &block|
|
|
27
27
|
super(*args, &block) if options[:call_super]
|
|
28
|
-
associations[metadata]
|
|
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
|