nobrainer 0.15.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/no_brainer/autoload.rb +2 -2
- data/lib/no_brainer/criteria.rb +3 -2
- data/lib/no_brainer/criteria/after_find.rb +1 -1
- data/lib/no_brainer/criteria/aggregate.rb +25 -0
- data/lib/no_brainer/criteria/core.rb +15 -1
- data/lib/no_brainer/criteria/count.rb +1 -1
- data/lib/no_brainer/criteria/delete.rb +2 -2
- data/lib/no_brainer/criteria/index.rb +37 -0
- data/lib/no_brainer/criteria/order_by.rb +53 -13
- data/lib/no_brainer/criteria/pluck.rb +81 -0
- data/lib/no_brainer/criteria/raw.rb +12 -4
- data/lib/no_brainer/criteria/scope.rb +11 -6
- data/lib/no_brainer/criteria/update.rb +12 -4
- data/lib/no_brainer/criteria/where.rb +133 -64
- data/lib/no_brainer/document.rb +4 -3
- data/lib/no_brainer/document/aliases.rb +55 -0
- data/lib/no_brainer/document/association.rb +1 -1
- data/lib/no_brainer/document/association/belongs_to.rb +2 -2
- data/lib/no_brainer/document/attributes.rb +26 -8
- data/lib/no_brainer/document/criteria.rb +7 -4
- data/lib/no_brainer/document/dirty.rb +21 -4
- data/lib/no_brainer/document/dynamic_attributes.rb +4 -1
- data/lib/no_brainer/document/index.rb +47 -12
- data/lib/no_brainer/document/lazy_fetch.rb +72 -0
- data/lib/no_brainer/document/missing_attributes.rb +73 -0
- data/lib/no_brainer/document/persistance.rb +57 -8
- data/lib/no_brainer/document/polymorphic.rb +14 -8
- data/lib/no_brainer/document/types.rb +5 -2
- data/lib/no_brainer/document/types/binary.rb +23 -0
- data/lib/no_brainer/document/uniqueness.rb +1 -1
- data/lib/no_brainer/error.rb +16 -0
- data/lib/no_brainer/index_manager.rb +1 -0
- data/lib/no_brainer/query_runner.rb +3 -1
- data/lib/no_brainer/query_runner/connection_lock.rb +7 -0
- data/lib/no_brainer/query_runner/database_on_demand.rb +6 -3
- data/lib/no_brainer/query_runner/logger.rb +22 -6
- data/lib/no_brainer/query_runner/missing_index.rb +5 -3
- data/lib/no_brainer/query_runner/table_on_demand.rb +6 -3
- data/lib/no_brainer/railtie.rb +1 -1
- metadata +12 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe36d41b56c9780a4d16f3085d31de0361d34cae
|
4
|
+
data.tar.gz: 9f89b0da1f34d65c8861345307adc79f93a5593d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43e0cf6f767028a46068635713cb9c1d639a8e03de6503493a6af05179977dfe1ae01b5c6a49d93ec0f2e4012a24b0386b6f66d31c7dd274ec05f5f69b252ba3
|
7
|
+
data.tar.gz: e728763fc6095d493db2ed93a9141f72d7cfc927f35300993f0ca6272996a5bac120f6f9cbfc9a985d80329a78da88ec5d827d5af31ccce93c833704b3a17f0b
|
data/lib/no_brainer/autoload.rb
CHANGED
@@ -15,11 +15,11 @@ module NoBrainer::Autoload
|
|
15
15
|
|
16
16
|
def autoload_and_include(*constants)
|
17
17
|
constants.each { |constant| autoload constant }
|
18
|
-
constants.each { |constant| include const_get
|
18
|
+
constants.each { |constant| include const_get(constant) }
|
19
19
|
end
|
20
20
|
|
21
21
|
def eager_autoload_and_include(*constants)
|
22
22
|
eager_autoload(*constants)
|
23
|
-
constants.each { |constant| include const_get
|
23
|
+
constants.each { |constant| include const_get(constant) }
|
24
24
|
end
|
25
25
|
end
|
data/lib/no_brainer/criteria.rb
CHANGED
@@ -2,6 +2,7 @@ require 'rethinkdb'
|
|
2
2
|
|
3
3
|
class NoBrainer::Criteria
|
4
4
|
extend NoBrainer::Autoload
|
5
|
-
autoload_and_include :Core, :
|
6
|
-
:Count, :Delete, :Enumerable, :First, :
|
5
|
+
autoload_and_include :Core, :Raw, :AfterFind, :Where, :OrderBy, :Limit,
|
6
|
+
:Pluck, :Count, :Delete, :Enumerable, :First, :Aggregate,
|
7
|
+
:Preload, :Update, :Cache, :Index, :Scope
|
7
8
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module NoBrainer::Criteria::Aggregate
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def min(*a, &b)
|
5
|
+
instantiate_doc NoBrainer.run { aggregate_rql(:min, *a, &b) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def max(*a, &b)
|
9
|
+
instantiate_doc NoBrainer.run { aggregate_rql(:max, *a, &b) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def sum(*a, &b)
|
13
|
+
NoBrainer.run { aggregate_rql(:sum, *a, &b) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def avg(*a, &b)
|
17
|
+
NoBrainer.run { aggregate_rql(:avg, *a, &b) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def aggregate_rql(type, *a, &b)
|
23
|
+
without_ordering.without_plucking.to_rql.__send__(type, *klass.with_fields_aliased(a), &b)
|
24
|
+
end
|
25
|
+
end
|
@@ -12,7 +12,7 @@ module NoBrainer::Criteria::Core
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def to_rql
|
15
|
-
|
15
|
+
finalized_criteria.__send__(:compile_rql_pass2)
|
16
16
|
end
|
17
17
|
|
18
18
|
def inspect
|
@@ -56,4 +56,18 @@ module NoBrainer::Criteria::Core
|
|
56
56
|
# This method is overriden by other modules.
|
57
57
|
compile_rql_pass1
|
58
58
|
end
|
59
|
+
|
60
|
+
def finalized?
|
61
|
+
!!init_options[:finalized]
|
62
|
+
end
|
63
|
+
|
64
|
+
def finalized_criteria
|
65
|
+
@finalized_criteria ||= finalized? ? self : self.class._finalize_criteria(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
module ClassMethods
|
69
|
+
def _finalize_criteria(base)
|
70
|
+
base.merge(base.class.new(:finalized => true))
|
71
|
+
end
|
72
|
+
end
|
59
73
|
end
|
@@ -2,10 +2,10 @@ module NoBrainer::Criteria::Delete
|
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
|
4
4
|
def delete_all
|
5
|
-
run(without_ordering.to_rql.delete)
|
5
|
+
run(without_ordering.without_plucking.to_rql.delete)
|
6
6
|
end
|
7
7
|
|
8
8
|
def destroy_all
|
9
|
-
|
9
|
+
to_a.each { |doc| doc.destroy }
|
10
10
|
end
|
11
11
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module NoBrainer::Criteria::Index
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included { attr_accessor :with_index_name }
|
5
|
+
|
6
|
+
def with_index(index_name=true)
|
7
|
+
chain { |criteria| criteria.with_index_name = index_name }
|
8
|
+
end
|
9
|
+
|
10
|
+
def without_index
|
11
|
+
with_index(false)
|
12
|
+
end
|
13
|
+
|
14
|
+
def without_index?
|
15
|
+
finalized_criteria.with_index_name == false
|
16
|
+
end
|
17
|
+
|
18
|
+
def used_index
|
19
|
+
# only one of them will be active.
|
20
|
+
where_index_name || order_by_index_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def merge!(criteria, options={})
|
24
|
+
super
|
25
|
+
self.with_index_name = criteria.with_index_name unless criteria.with_index_name.nil?
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def compile_rql_pass2
|
30
|
+
super.tap do
|
31
|
+
if with_index_name && (!used_index || order_by_index_name.to_s == klass.pk_name.to_s)
|
32
|
+
# The implicit ordering on the indexed pk does not count.
|
33
|
+
raise NoBrainer::Error::CannotUseIndex.new(with_index_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -16,8 +16,7 @@ module NoBrainer::Criteria::OrderBy
|
|
16
16
|
bad_rule = rule.values.reject { |v| v.in? [:asc, :desc] }.first
|
17
17
|
raise_bad_rule(bad_rule) if bad_rule
|
18
18
|
rule
|
19
|
-
when Symbol then { rule => :asc }
|
20
|
-
when Proc then { rule => :asc }
|
19
|
+
when String, Symbol, Proc then { rule => :asc }
|
21
20
|
else raise_bad_rule(rule)
|
22
21
|
end
|
23
22
|
end.reduce({}, :merge)
|
@@ -52,6 +51,14 @@ module NoBrainer::Criteria::OrderBy
|
|
52
51
|
end
|
53
52
|
end
|
54
53
|
|
54
|
+
def order_by_indexed?
|
55
|
+
!!order_by_index_name
|
56
|
+
end
|
57
|
+
|
58
|
+
def order_by_index_name
|
59
|
+
order_by_index_finder.index_name
|
60
|
+
end
|
61
|
+
|
55
62
|
private
|
56
63
|
|
57
64
|
def effective_order
|
@@ -66,6 +73,41 @@ module NoBrainer::Criteria::OrderBy
|
|
66
73
|
self.ordering_mode != :disabled
|
67
74
|
end
|
68
75
|
|
76
|
+
class IndexFinder < Struct.new(:criteria, :index_name, :rql_proc)
|
77
|
+
def could_find_index?
|
78
|
+
!!self.index_name
|
79
|
+
end
|
80
|
+
|
81
|
+
def first_key
|
82
|
+
@first_key ||= criteria.__send__(:effective_order).first.try(:[], 0)
|
83
|
+
end
|
84
|
+
|
85
|
+
def first_key_indexable?
|
86
|
+
(first_key.is_a?(Symbol) || first_key.is_a?(String)) && criteria.klass.has_index?(first_key)
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_index
|
90
|
+
return if criteria.without_index?
|
91
|
+
return unless first_key_indexable?
|
92
|
+
|
93
|
+
if criteria.with_index_name && criteria.with_index_name != true
|
94
|
+
return unless first_key.to_s == criteria.with_index_name.to_s
|
95
|
+
end
|
96
|
+
|
97
|
+
# We need make sure that the where index finder has been invoked, it has priority.
|
98
|
+
# If it doesn't find anything, we are free to go with our indexes.
|
99
|
+
if !criteria.where_indexed? || (criteria.where_index_type == :between &&
|
100
|
+
first_key.to_s == criteria.where_index_name.to_s)
|
101
|
+
self.index_name = first_key
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def order_by_index_finder
|
107
|
+
return finalized_criteria.__send__(:order_by_index_finder) unless finalized?
|
108
|
+
@order_by_index_finder ||= IndexFinder.new(self).tap { |index_finder| index_finder.find_index }
|
109
|
+
end
|
110
|
+
|
69
111
|
def compile_rql_pass1
|
70
112
|
rql = super
|
71
113
|
return rql unless should_order?
|
@@ -73,6 +115,12 @@ module NoBrainer::Criteria::OrderBy
|
|
73
115
|
return rql if _effective_order.empty?
|
74
116
|
|
75
117
|
rql_rules = _effective_order.map do |k,v|
|
118
|
+
if order_by_index_finder.index_name == k
|
119
|
+
k = klass.lookup_index_alias(k)
|
120
|
+
else
|
121
|
+
k = klass.lookup_field_alias(k)
|
122
|
+
end
|
123
|
+
|
76
124
|
case v
|
77
125
|
when :asc then reverse_order? ? RethinkDB::RQL.new.desc(k) : RethinkDB::RQL.new.asc(k)
|
78
126
|
when :desc then reverse_order? ? RethinkDB::RQL.new.asc(k) : RethinkDB::RQL.new.desc(k)
|
@@ -82,19 +130,11 @@ module NoBrainer::Criteria::OrderBy
|
|
82
130
|
# We can only apply an index order_by on a table() term.
|
83
131
|
# We are going to try to go so and if we cannot, we'll simply apply
|
84
132
|
# the ordering in pass2, which will happen after a potential filter().
|
85
|
-
|
86
|
-
|
87
|
-
if NoBrainer::RQL.is_table?(rql) && !without_index?
|
88
|
-
options = {}
|
89
|
-
first_key = _effective_order.first[0]
|
90
|
-
if (first_key.is_a?(Symbol) || first_key.is_a?(String)) && klass.has_index?(first_key)
|
91
|
-
options[:index] = rql_rules.shift
|
92
|
-
end
|
93
|
-
|
133
|
+
if order_by_index_finder.could_find_index?
|
134
|
+
options = { :index => rql_rules.shift }
|
94
135
|
rql = rql.order_by(*rql_rules, options)
|
95
136
|
else
|
96
|
-
# Stashing @rql_rules for pass2
|
97
|
-
# We should really use more of a middleware pattern to build the RQL.
|
137
|
+
# Stashing @rql_rules for pass2
|
98
138
|
@rql_rules_pass2 = rql_rules
|
99
139
|
end
|
100
140
|
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module NoBrainer::Criteria::Pluck
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included { attr_accessor :missing_attributes }
|
5
|
+
|
6
|
+
def pluck(*attrs)
|
7
|
+
_missing_attributes_criteria(:pluck, attrs)
|
8
|
+
end
|
9
|
+
|
10
|
+
def without(*attrs)
|
11
|
+
_missing_attributes_criteria(:without, attrs)
|
12
|
+
end
|
13
|
+
|
14
|
+
def lazy_fetch(*attrs)
|
15
|
+
_missing_attributes_criteria(:lazy_fetch, attrs)
|
16
|
+
end
|
17
|
+
|
18
|
+
def without_plucking
|
19
|
+
chain { |criteria| criteria.missing_attributes = :remove }
|
20
|
+
end
|
21
|
+
|
22
|
+
def merge!(criteria, options={})
|
23
|
+
return super unless criteria.missing_attributes
|
24
|
+
|
25
|
+
if criteria.missing_attributes == :remove
|
26
|
+
self.missing_attributes = nil
|
27
|
+
else
|
28
|
+
self.missing_attributes ||= {}
|
29
|
+
criteria.missing_attributes.each do |type, attrs|
|
30
|
+
old_attrs = self.missing_attributes[type] || {}.with_indifferent_access
|
31
|
+
new_attrs = old_attrs.deep_merge(attrs)
|
32
|
+
self.missing_attributes[type] = new_attrs
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def _missing_attributes_criteria(type, args)
|
42
|
+
raise ArgumentError if args.size.zero?
|
43
|
+
args = [Hash[args.flatten.map { |k| [k, true] }]] unless args.size == 1 && args.first.is_a?(Hash)
|
44
|
+
chain { |criteria| criteria.missing_attributes = {type => args.first} }
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
def effective_missing_attributes
|
49
|
+
return nil if self.missing_attributes.nil?
|
50
|
+
@effective_missing_attributes ||= begin
|
51
|
+
# pluck gets priority
|
52
|
+
|
53
|
+
missing_attributes = Hash[self.missing_attributes.map do |type, attrs|
|
54
|
+
attrs = attrs.select { |k,v| v } # TODO recursive
|
55
|
+
attrs.empty? ? nil : [type, attrs]
|
56
|
+
end.compact]
|
57
|
+
|
58
|
+
if missing_attributes[:pluck]
|
59
|
+
{ :pluck => missing_attributes[:pluck] }
|
60
|
+
else
|
61
|
+
attrs = [missing_attributes[:without], missing_attributes[:lazy_fetch]].compact.reduce(&:merge)
|
62
|
+
{ :without => attrs } if attrs.present?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def _instantiate_model(attrs, options={})
|
68
|
+
return super if missing_attributes.nil?
|
69
|
+
super(attrs, options.merge(:missing_attributes => effective_missing_attributes,
|
70
|
+
:lazy_fetch => missing_attributes[:lazy_fetch]))
|
71
|
+
end
|
72
|
+
|
73
|
+
def compile_rql_pass2
|
74
|
+
rql = super
|
75
|
+
if effective_missing_attributes
|
76
|
+
type, attrs = effective_missing_attributes.first
|
77
|
+
rql = rql.__send__(type, klass.with_fields_aliased(attrs))
|
78
|
+
end
|
79
|
+
rql
|
80
|
+
end
|
81
|
+
end
|
@@ -13,13 +13,21 @@ module NoBrainer::Criteria::Raw
|
|
13
13
|
self
|
14
14
|
end
|
15
15
|
|
16
|
-
private
|
17
|
-
|
18
16
|
def raw?
|
19
|
-
!!_raw
|
17
|
+
!!finalized_criteria._raw
|
20
18
|
end
|
21
19
|
|
20
|
+
private
|
21
|
+
|
22
22
|
def instantiate_doc(attrs)
|
23
|
-
|
23
|
+
finalized_criteria._instantiate_doc(attrs)
|
24
|
+
end
|
25
|
+
|
26
|
+
def _instantiate_doc(attrs)
|
27
|
+
raw? ? attrs : _instantiate_model(attrs)
|
28
|
+
end
|
29
|
+
|
30
|
+
def _instantiate_model(attrs, options={})
|
31
|
+
klass.new_from_db(attrs, options)
|
24
32
|
end
|
25
33
|
end
|
@@ -34,12 +34,17 @@ module NoBrainer::Criteria::Scope
|
|
34
34
|
klass.default_scope_proc && use_default_scope != false
|
35
35
|
end
|
36
36
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
37
|
+
def _apply_default_scope
|
38
|
+
return unless should_apply_default_scope?
|
39
|
+
criteria = klass.default_scope_proc.call
|
40
|
+
raise "Mixing model issue. Contact developer." if [criteria.klass, self.klass].compact.uniq.size == 2
|
41
|
+
criteria.merge(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
def _finalize_criteria(base)
|
46
|
+
criteria = super
|
47
|
+
criteria.__send__(:_apply_default_scope) || criteria
|
43
48
|
end
|
44
49
|
end
|
45
50
|
end
|
@@ -1,11 +1,19 @@
|
|
1
1
|
module NoBrainer::Criteria::Update
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
|
4
|
-
def update_all(*
|
5
|
-
|
4
|
+
def update_all(*a, &b)
|
5
|
+
prepare_args_for_update!(a)
|
6
|
+
run(without_ordering.without_plucking.to_rql.update(*a, &b))
|
6
7
|
end
|
7
8
|
|
8
|
-
def replace_all(*
|
9
|
-
|
9
|
+
def replace_all(*a, &b)
|
10
|
+
prepare_args_for_update!(a)
|
11
|
+
run(without_ordering.without_plucking.to_rql.replace(*a, &b))
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def prepare_args_for_update!(a)
|
17
|
+
a[0] = klass.persistable_attributes(a[0]) if !a.empty? && a.first.is_a?(Hash)
|
10
18
|
end
|
11
19
|
end
|
@@ -5,45 +5,60 @@ module NoBrainer::Criteria::Where
|
|
5
5
|
|
6
6
|
def initialize(options={})
|
7
7
|
super
|
8
|
-
self.where_ast = MultiOperator.new(:and, [])
|
9
8
|
end
|
10
9
|
|
11
10
|
def where(*args, &block)
|
12
11
|
chain { |criteria| criteria.where_ast = parse_clause([*args, block].compact) }
|
13
12
|
end
|
14
13
|
|
15
|
-
def
|
16
|
-
|
14
|
+
def merge!(criteria, options={})
|
15
|
+
super
|
16
|
+
|
17
|
+
if criteria.where_ast
|
18
|
+
if self.where_ast
|
19
|
+
self.where_ast = MultiOperator.new(:and, [self.where_ast, criteria.where_ast])
|
20
|
+
else
|
21
|
+
self.where_ast = criteria.where_ast
|
22
|
+
end
|
23
|
+
self.where_ast = self.where_ast.simplify
|
24
|
+
raise unless criteria.where_ast.is_a?(MultiOperator)
|
25
|
+
end
|
26
|
+
|
27
|
+
self.with_index_name = criteria.with_index_name unless criteria.with_index_name.nil?
|
28
|
+
self
|
17
29
|
end
|
18
30
|
|
19
|
-
def
|
20
|
-
|
31
|
+
def where_present?
|
32
|
+
finalized_criteria.where_ast.try(:clauses).present?
|
21
33
|
end
|
22
34
|
|
23
|
-
def
|
24
|
-
|
35
|
+
def where_indexed?
|
36
|
+
!!where_index_name
|
25
37
|
end
|
26
38
|
|
27
|
-
def
|
28
|
-
|
39
|
+
def where_index_name
|
40
|
+
where_index_finder.index_name
|
29
41
|
end
|
30
42
|
|
31
|
-
def
|
32
|
-
|
33
|
-
clauses = self.where_ast.clauses + criteria.where_ast.clauses
|
34
|
-
self.where_ast = MultiOperator.new(:and, clauses).simplify
|
35
|
-
self.with_index_name = criteria.with_index_name unless criteria.with_index_name.nil?
|
36
|
-
self
|
43
|
+
def where_index_type
|
44
|
+
where_index_finder.index_type
|
37
45
|
end
|
38
46
|
|
39
47
|
private
|
40
48
|
|
41
49
|
class MultiOperator < Struct.new(:op, :clauses)
|
42
50
|
def simplify
|
43
|
-
|
44
|
-
|
51
|
+
clauses = self.clauses.map(&:simplify)
|
52
|
+
if self.clauses.size == 1 && self.clauses.first.is_a?(self.class)
|
53
|
+
return clauses.first
|
54
|
+
end
|
55
|
+
|
56
|
+
same_op_clauses, other_clauses = clauses.partition do |v|
|
57
|
+
v.is_a?(self.class) && (v.clauses.size == 1 || v.op == self.op)
|
58
|
+
end
|
45
59
|
simplified_clauses = other_clauses + same_op_clauses.map(&:clauses).flatten(1)
|
46
|
-
|
60
|
+
simplified_clauses = BinaryOperator.simplify_clauses(op, simplified_clauses.uniq)
|
61
|
+
self.class.new(op, simplified_clauses)
|
47
62
|
end
|
48
63
|
|
49
64
|
def to_rql(doc)
|
@@ -54,22 +69,48 @@ module NoBrainer::Criteria::Where
|
|
54
69
|
end
|
55
70
|
end
|
56
71
|
|
57
|
-
class BinaryOperator < Struct.new(:key, :op, :value, :
|
72
|
+
class BinaryOperator < Struct.new(:key, :op, :value, :model, :casted_values)
|
73
|
+
def self.get_candidate_clauses(clauses, *types)
|
74
|
+
clauses.select { |c| c.is_a?(self) && types.include?(c.op) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.simplify_clauses(op, ast_clauses)
|
78
|
+
# This code assumes that simplfy() has already been called on all clauses.
|
79
|
+
if op == :or
|
80
|
+
eq_clauses = get_candidate_clauses(ast_clauses, :in, :eq)
|
81
|
+
new_clauses = eq_clauses.group_by(&:key).map do |key, clauses|
|
82
|
+
case clauses.size
|
83
|
+
when 1 then clauses.first
|
84
|
+
else
|
85
|
+
values = clauses.map { |c| c.op == :in ? c.value : [c.value] }.flatten(1).uniq
|
86
|
+
BinaryOperator.new(key, :in, values, clauses.first.model, true)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if new_clauses.size != eq_clauses.size
|
91
|
+
ast_clauses = ast_clauses - eq_clauses + new_clauses
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
ast_clauses
|
96
|
+
end
|
97
|
+
|
58
98
|
def simplify
|
59
99
|
key = cast_key(self.key)
|
60
100
|
case op
|
61
101
|
when :in then
|
62
102
|
case value
|
63
|
-
when Range then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)),
|
64
|
-
when Array then BinaryOperator.new(key, :in, value.map(&method(:cast_value)).uniq,
|
103
|
+
when Range then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), model, true)
|
104
|
+
when Array then BinaryOperator.new(key, :in, value.map(&method(:cast_value)).uniq, model, true)
|
65
105
|
else raise ArgumentError.new ":in takes an array/range, not #{value}"
|
66
106
|
end
|
67
|
-
when :between then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)),
|
68
|
-
else BinaryOperator.new(key, op, cast_value(value),
|
107
|
+
when :between then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), model, true)
|
108
|
+
else BinaryOperator.new(key, op, cast_value(value), model, true)
|
69
109
|
end
|
70
110
|
end
|
71
111
|
|
72
112
|
def to_rql(doc)
|
113
|
+
key = model.lookup_field_alias(self.key)
|
73
114
|
case op
|
74
115
|
when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
|
75
116
|
when :between then (doc[key] >= value.min) & (doc[key] <= value.max)
|
@@ -84,7 +125,7 @@ module NoBrainer::Criteria::Where
|
|
84
125
|
# FIXME This leaks memory with dynamic attributes. The internals of type
|
85
126
|
# checking will convert the key to a symbol, and Ruby does not garbage
|
86
127
|
# collect symbols.
|
87
|
-
@association ||= [
|
128
|
+
@association ||= [model.association_metadata[key.to_sym]]
|
88
129
|
@association.first
|
89
130
|
end
|
90
131
|
|
@@ -98,7 +139,7 @@ module NoBrainer::Criteria::Where
|
|
98
139
|
raise NoBrainer::Error::InvalidType.new(opts) unless value.is_a?(target_klass)
|
99
140
|
value.pk_value
|
100
141
|
else
|
101
|
-
|
142
|
+
model.cast_user_to_db_for(key, value)
|
102
143
|
end
|
103
144
|
end
|
104
145
|
|
@@ -106,10 +147,8 @@ module NoBrainer::Criteria::Where
|
|
106
147
|
return key if casted_values
|
107
148
|
|
108
149
|
case association
|
109
|
-
when NoBrainer::Document::Association::BelongsTo::Metadata
|
110
|
-
|
111
|
-
else
|
112
|
-
key
|
150
|
+
when NoBrainer::Document::Association::BelongsTo::Metadata then association.foreign_key
|
151
|
+
else key
|
113
152
|
end
|
114
153
|
end
|
115
154
|
end
|
@@ -161,7 +200,7 @@ module NoBrainer::Criteria::Where
|
|
161
200
|
when :nin then parse_clause(:not => { key.symbol.in => value })
|
162
201
|
when :ne then parse_clause(:not => { key.symbol.eq => value })
|
163
202
|
when :eq then parse_clause_stub_eq(key.symbol, value)
|
164
|
-
else BinaryOperator.new(key.symbol, key.modifier, value, self)
|
203
|
+
else BinaryOperator.new(key.symbol, key.modifier, value, self.klass)
|
165
204
|
end
|
166
205
|
else raise "Invalid key: #{key}"
|
167
206
|
end
|
@@ -169,37 +208,40 @@ module NoBrainer::Criteria::Where
|
|
169
208
|
|
170
209
|
def parse_clause_stub_eq(key, value)
|
171
210
|
case value
|
172
|
-
when Range then BinaryOperator.new(key, :between, value, self)
|
173
|
-
when Regexp then BinaryOperator.new(key, :match, value.inspect[1..-2], self)
|
174
|
-
else BinaryOperator.new(key, :eq, value, self)
|
211
|
+
when Range then BinaryOperator.new(key, :between, value, self.klass)
|
212
|
+
when Regexp then BinaryOperator.new(key, :match, value.inspect[1..-2], self.klass)
|
213
|
+
else BinaryOperator.new(key, :eq, value, self.klass)
|
175
214
|
end
|
176
215
|
end
|
177
216
|
|
178
|
-
|
179
|
-
self.with_index_name == false
|
180
|
-
end
|
181
|
-
|
182
|
-
class IndexFinder < Struct.new(:criteria, :index_name, :rql_proc, :ast)
|
217
|
+
class IndexFinder < Struct.new(:criteria, :ast, :index_name, :index_type, :rql_proc)
|
183
218
|
def initialize(*args)
|
184
219
|
super
|
185
|
-
find_index
|
186
220
|
end
|
187
221
|
|
188
222
|
def could_find_index?
|
189
223
|
!!self.index_name
|
190
224
|
end
|
191
225
|
|
192
|
-
private
|
193
|
-
|
194
226
|
def get_candidate_clauses(*types)
|
195
|
-
|
227
|
+
BinaryOperator.get_candidate_clauses(ast.clauses, *types)
|
196
228
|
end
|
197
229
|
|
198
230
|
def get_usable_indexes(*types)
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
231
|
+
@usable_indexes = {}
|
232
|
+
@usable_indexes[types] ||= begin
|
233
|
+
indexes = criteria.klass.indexes
|
234
|
+
indexes = indexes.select { |k,v| types.include?(v[:kind]) } if types.present?
|
235
|
+
if criteria.with_index_name && criteria.with_index_name != true
|
236
|
+
indexes = indexes.select { |k,v| k == criteria.with_index_name.to_sym }
|
237
|
+
end
|
238
|
+
indexes
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def remove_from_ast(clauses)
|
243
|
+
new_ast = MultiOperator.new(ast.op, ast.clauses - clauses)
|
244
|
+
return new_ast if new_ast.clauses.present?
|
203
245
|
end
|
204
246
|
|
205
247
|
def find_index_canonical
|
@@ -208,12 +250,14 @@ module NoBrainer::Criteria::Where
|
|
208
250
|
|
209
251
|
if index_name = (get_usable_indexes.keys & clauses.keys).first
|
210
252
|
clause = clauses[index_name]
|
253
|
+
aliased_index = criteria.klass.indexes[index_name][:as]
|
211
254
|
self.index_name = index_name
|
212
|
-
self.ast =
|
255
|
+
self.ast = remove_from_ast([clause])
|
256
|
+
self.index_type = clause.op == :between ? :between : :get_all
|
213
257
|
self.rql_proc = case clause.op
|
214
|
-
when :eq then ->(rql){ rql.get_all(clause.value, :index =>
|
215
|
-
when :in then ->(rql){ rql.get_all(*clause.value, :index =>
|
216
|
-
when :between then ->(rql){ rql.between(clause.value.min, clause.value.max, :index =>
|
258
|
+
when :eq then ->(rql){ rql.get_all(clause.value, :index => aliased_index) }
|
259
|
+
when :in then ->(rql){ rql.get_all(*clause.value, :index => aliased_index) }
|
260
|
+
when :between then ->(rql){ rql.between(clause.value.min, clause.value.max, :index => aliased_index,
|
217
261
|
:left_bound => :closed, :right_bound => :closed) }
|
218
262
|
end
|
219
263
|
end
|
@@ -230,9 +274,11 @@ module NoBrainer::Criteria::Where
|
|
230
274
|
|
231
275
|
if index_name
|
232
276
|
indexed_clauses = index_values.map { |field| clauses[field] }
|
277
|
+
aliased_index = criteria.klass.indexes[index_name][:as]
|
233
278
|
self.index_name = index_name
|
234
|
-
self.ast =
|
235
|
-
self.
|
279
|
+
self.ast = remove_from_ast(indexed_clauses)
|
280
|
+
self.index_type = :get_all
|
281
|
+
self.rql_proc = ->(rql){ rql.get_all(indexed_clauses.map { |c| c.value }, :index => aliased_index) }
|
236
282
|
end
|
237
283
|
end
|
238
284
|
|
@@ -245,40 +291,63 @@ module NoBrainer::Criteria::Where
|
|
245
291
|
left_bound = op_clauses[:gt] || op_clauses[:ge]
|
246
292
|
right_bound = op_clauses[:lt] || op_clauses[:le]
|
247
293
|
|
294
|
+
aliased_index = criteria.klass.indexes[index_name][:as]
|
248
295
|
self.index_name = index_name
|
249
|
-
self.ast =
|
296
|
+
self.ast = remove_from_ast([left_bound, right_bound].compact)
|
250
297
|
|
251
|
-
options = {
|
298
|
+
options = {}
|
299
|
+
options[:index] = aliased_index
|
252
300
|
options[:left_bound] = {:gt => :open, :ge => :closed}[left_bound.op] if left_bound
|
253
301
|
options[:right_bound] = {:lt => :open, :le => :closed}[right_bound.op] if right_bound
|
302
|
+
self.index_type = :between
|
254
303
|
self.rql_proc = ->(rql){ rql.between(left_bound.try(:value), right_bound.try(:value), options) }
|
255
304
|
end
|
256
305
|
end
|
257
306
|
|
307
|
+
def find_union_index
|
308
|
+
indexes = []
|
309
|
+
index_finder = self
|
310
|
+
|
311
|
+
loop do
|
312
|
+
index_finder = index_finder.dup
|
313
|
+
break unless index_finder.find_index_canonical
|
314
|
+
# TODO To use a compound index, we'd have to add all permutations in the query
|
315
|
+
indexes << index_finder
|
316
|
+
break unless index_finder.ast
|
317
|
+
end
|
318
|
+
|
319
|
+
if indexes.present? && !index_finder.ast
|
320
|
+
self.ast = nil
|
321
|
+
self.index_name = indexes.map(&:index_name)
|
322
|
+
self.index_type = indexes.map(&:index_type)
|
323
|
+
self.rql_proc = ->(rql){ indexes.map { |index| index.rql_proc.call(rql) }.reduce { |a,b| a.union(b) } }
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
258
327
|
def find_index
|
259
|
-
return if criteria.
|
260
|
-
|
261
|
-
|
262
|
-
|
328
|
+
return if ast.nil? || criteria.without_index?
|
329
|
+
case ast.op
|
330
|
+
when :and then find_index_compound || find_index_canonical || find_index_hidden_between
|
331
|
+
when :or then find_union_index
|
263
332
|
end
|
264
333
|
end
|
265
334
|
end
|
266
335
|
|
267
|
-
def
|
268
|
-
return
|
269
|
-
@
|
336
|
+
def where_index_finder
|
337
|
+
return finalized_criteria.__send__(:where_index_finder) unless finalized?
|
338
|
+
@where_index_finder ||= IndexFinder.new(self, where_ast).tap { |index_finder| index_finder.find_index }
|
270
339
|
end
|
271
340
|
|
272
341
|
def compile_rql_pass1
|
273
342
|
rql = super
|
274
|
-
rql =
|
343
|
+
rql = where_index_finder.rql_proc.call(rql) if where_index_finder.could_find_index?
|
275
344
|
rql
|
276
345
|
end
|
277
346
|
|
278
347
|
def compile_rql_pass2
|
279
348
|
rql = super
|
280
|
-
ast =
|
281
|
-
rql = rql.filter { |doc| ast.to_rql(doc) } if ast.clauses.present?
|
349
|
+
ast = where_index_finder.could_find_index? ? where_index_finder.ast : self.where_ast
|
350
|
+
rql = rql.filter { |doc| ast.to_rql(doc) } if ast.try(:clauses).present?
|
282
351
|
rql
|
283
352
|
end
|
284
353
|
end
|