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