nobrainer 0.15.0 → 0.16.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 +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
|