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.
- checksums.yaml +4 -4
- data/lib/no_brainer/autoload.rb +1 -1
- data/lib/no_brainer/config.rb +54 -14
- data/lib/no_brainer/connection.rb +24 -21
- data/lib/no_brainer/connection_manager.rb +29 -9
- data/lib/no_brainer/criteria/cache.rb +8 -0
- data/lib/no_brainer/criteria/changes.rb +16 -0
- data/lib/no_brainer/criteria/core.rb +4 -5
- data/lib/no_brainer/criteria/eager_load.rb +2 -2
- data/lib/no_brainer/criteria/enumerable.rb +3 -1
- data/lib/no_brainer/criteria/find.rb +6 -9
- data/lib/no_brainer/criteria/first.rb +3 -3
- data/lib/no_brainer/criteria/first_or_create.rb +114 -0
- data/lib/no_brainer/criteria/join.rb +62 -0
- data/lib/no_brainer/criteria/order_by.rb +19 -16
- data/lib/no_brainer/criteria/run.rb +26 -0
- data/lib/no_brainer/criteria/scope.rb +8 -8
- data/lib/no_brainer/criteria/where.rb +130 -107
- data/lib/no_brainer/criteria.rb +4 -4
- data/lib/no_brainer/document/association/belongs_to.rb +44 -17
- data/lib/no_brainer/document/association/core.rb +11 -0
- data/lib/no_brainer/document/association/eager_loader.rb +26 -26
- data/lib/no_brainer/document/association/has_many.rb +7 -9
- data/lib/no_brainer/document/association/has_many_through.rb +1 -2
- data/lib/no_brainer/document/association/has_one.rb +5 -1
- data/lib/no_brainer/document/association.rb +5 -5
- data/lib/no_brainer/document/atomic_ops.rb +40 -7
- data/lib/no_brainer/document/attributes.rb +24 -15
- data/lib/no_brainer/document/callbacks.rb +1 -1
- data/lib/no_brainer/document/core.rb +18 -18
- data/lib/no_brainer/document/criteria.rb +9 -5
- data/lib/no_brainer/document/dirty.rb +15 -12
- data/lib/no_brainer/document/dynamic_attributes.rb +6 -0
- data/lib/no_brainer/document/index/index.rb +5 -9
- data/lib/no_brainer/document/index/meta_store.rb +1 -10
- data/lib/no_brainer/document/index/synchronizer.rb +16 -20
- data/lib/no_brainer/document/index.rb +3 -3
- data/lib/no_brainer/document/lazy_fetch.rb +3 -3
- data/lib/no_brainer/document/missing_attributes.rb +7 -2
- data/lib/no_brainer/document/persistance.rb +14 -30
- data/lib/no_brainer/document/polymorphic.rb +8 -4
- data/lib/no_brainer/document/primary_key/generator.rb +6 -1
- data/lib/no_brainer/document/primary_key.rb +19 -4
- data/lib/no_brainer/document/serialization.rb +0 -2
- data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
- data/lib/no_brainer/document/table_config.rb +118 -0
- data/lib/no_brainer/document/timestamps.rb +8 -0
- data/lib/no_brainer/document/validation/core.rb +63 -0
- data/lib/no_brainer/document/validation/uniqueness.rb +30 -36
- data/lib/no_brainer/document/validation.rb +1 -58
- data/lib/no_brainer/document.rb +2 -2
- data/lib/no_brainer/error.rb +1 -0
- data/lib/no_brainer/geo/base.rb +0 -1
- data/lib/no_brainer/locale/en.yml +1 -0
- data/lib/no_brainer/lock.rb +12 -8
- data/lib/no_brainer/profiler/controller_runtime.rb +76 -0
- data/lib/no_brainer/{query_runner → profiler}/logger.rb +11 -27
- data/lib/no_brainer/profiler.rb +11 -0
- data/lib/no_brainer/query_runner/database_on_demand.rb +8 -8
- data/lib/no_brainer/query_runner/driver.rb +3 -1
- data/lib/no_brainer/query_runner/missing_index.rb +3 -3
- data/lib/no_brainer/query_runner/profiler.rb +43 -0
- data/lib/no_brainer/query_runner/reconnect.rb +38 -23
- data/lib/no_brainer/query_runner/run_options.rb +35 -15
- data/lib/no_brainer/query_runner/table_on_demand.rb +18 -11
- data/lib/no_brainer/query_runner.rb +3 -3
- data/lib/no_brainer/railtie/database.rake +14 -4
- data/lib/no_brainer/railtie.rb +5 -12
- data/lib/no_brainer/rql.rb +11 -8
- data/lib/no_brainer/symbol_decoration.rb +11 -0
- data/lib/no_brainer/system/cluster_config.rb +5 -0
- data/lib/no_brainer/system/db_config.rb +5 -0
- data/lib/no_brainer/system/document.rb +24 -0
- data/lib/no_brainer/system/issue.rb +10 -0
- data/lib/no_brainer/system/job.rb +10 -0
- data/lib/no_brainer/system/log.rb +11 -0
- data/lib/no_brainer/system/server_config.rb +7 -0
- data/lib/no_brainer/system/server_status.rb +9 -0
- data/lib/no_brainer/system/stat.rb +11 -0
- data/lib/no_brainer/system/table_config.rb +10 -0
- data/lib/no_brainer/system/table_status.rb +8 -0
- data/lib/no_brainer/system.rb +17 -0
- data/lib/nobrainer.rb +16 -11
- data/lib/rails/generators/nobrainer/install_generator.rb +48 -0
- data/lib/rails/generators/nobrainer/{model/model_generator.rb → model_generator.rb} +7 -3
- data/lib/rails/generators/nobrainer/namespace_fix.rb +15 -0
- data/lib/rails/generators/{nobrainer/model/templates/model.rb.tt → templates/model.rb} +1 -1
- data/lib/rails/generators/templates/nobrainer.rb +101 -0
- metadata +34 -10
- data/lib/no_brainer/document/store_in.rb +0 -35
- 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, :
|
|
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
|
-
|
|
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(:
|
|
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
|
|
47
|
-
@options[:
|
|
45
|
+
def ordering_mode
|
|
46
|
+
@options[:ordering_mode] || :implicit
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
def reverse_order?
|
|
51
|
-
|
|
50
|
+
!!@options[:reversed_ordering]
|
|
52
51
|
end
|
|
53
52
|
|
|
54
|
-
def
|
|
55
|
-
|
|
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
|
|
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
|
-
|
|
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__(:
|
|
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.
|
|
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(:
|
|
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.
|
|
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.
|
|
85
|
-
[BinaryOperator.new(
|
|
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
|
-
|
|
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 :
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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 =>
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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!(
|
|
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, :
|
|
218
|
+
class UnaryOperator < Struct.new(:op, :clause)
|
|
216
219
|
def simplify
|
|
217
|
-
|
|
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
|
|
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,
|
|
257
|
-
when :_or then parse_multi_value(:or, value,
|
|
258
|
-
when :not then UnaryOperator.new(:not, parse_clause(value))
|
|
259
|
-
when String, Symbol then
|
|
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
|
|
263
|
-
when :
|
|
264
|
-
when :
|
|
265
|
-
|
|
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) && !
|
|
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
|
|
309
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
361
|
-
|
|
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.
|
|
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(&:
|
|
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)
|
|
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
|
data/lib/no_brainer/criteria.rb
CHANGED
|
@@ -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,
|
|
6
|
-
:Pluck, :Count, :Delete, :Enumerable, :
|
|
7
|
-
:
|
|
8
|
-
:Extend, :
|
|
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
|