nobrainer 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +7 -0
  3. data/README.md +6 -0
  4. data/lib/no_brainer/autoload.rb +14 -0
  5. data/lib/no_brainer/config.rb +61 -0
  6. data/lib/no_brainer/connection.rb +53 -0
  7. data/lib/no_brainer/criteria.rb +20 -0
  8. data/lib/no_brainer/criteria/chainable/core.rb +67 -0
  9. data/lib/no_brainer/criteria/chainable/limit.rb +31 -0
  10. data/lib/no_brainer/criteria/chainable/order_by.rb +76 -0
  11. data/lib/no_brainer/criteria/chainable/raw.rb +25 -0
  12. data/lib/no_brainer/criteria/chainable/scope.rb +41 -0
  13. data/lib/no_brainer/criteria/chainable/where.rb +198 -0
  14. data/lib/no_brainer/criteria/termination/cache.rb +71 -0
  15. data/lib/no_brainer/criteria/termination/count.rb +19 -0
  16. data/lib/no_brainer/criteria/termination/delete.rb +11 -0
  17. data/lib/no_brainer/criteria/termination/eager_loading.rb +64 -0
  18. data/lib/no_brainer/criteria/termination/enumerable.rb +24 -0
  19. data/lib/no_brainer/criteria/termination/first.rb +25 -0
  20. data/lib/no_brainer/criteria/termination/inc.rb +14 -0
  21. data/lib/no_brainer/criteria/termination/update.rb +13 -0
  22. data/lib/no_brainer/database.rb +41 -0
  23. data/lib/no_brainer/decorated_symbol.rb +15 -0
  24. data/lib/no_brainer/document.rb +18 -0
  25. data/lib/no_brainer/document/association.rb +41 -0
  26. data/lib/no_brainer/document/association/belongs_to.rb +64 -0
  27. data/lib/no_brainer/document/association/core.rb +64 -0
  28. data/lib/no_brainer/document/association/has_many.rb +68 -0
  29. data/lib/no_brainer/document/attributes.rb +124 -0
  30. data/lib/no_brainer/document/core.rb +20 -0
  31. data/lib/no_brainer/document/criteria.rb +62 -0
  32. data/lib/no_brainer/document/dirty.rb +88 -0
  33. data/lib/no_brainer/document/dynamic_attributes.rb +12 -0
  34. data/lib/no_brainer/document/id.rb +49 -0
  35. data/lib/no_brainer/document/index.rb +102 -0
  36. data/lib/no_brainer/document/injection_layer.rb +12 -0
  37. data/lib/no_brainer/document/persistance.rb +124 -0
  38. data/lib/no_brainer/document/polymorphic.rb +43 -0
  39. data/lib/no_brainer/document/serialization.rb +9 -0
  40. data/lib/no_brainer/document/store_in.rb +33 -0
  41. data/lib/no_brainer/document/timestamps.rb +18 -0
  42. data/lib/no_brainer/document/validation.rb +35 -0
  43. data/lib/no_brainer/error.rb +10 -0
  44. data/lib/no_brainer/fork.rb +14 -0
  45. data/lib/no_brainer/index_manager.rb +6 -0
  46. data/lib/no_brainer/loader.rb +5 -0
  47. data/lib/no_brainer/locale/en.yml +4 -0
  48. data/lib/no_brainer/query_runner.rb +37 -0
  49. data/lib/no_brainer/query_runner/connection.rb +17 -0
  50. data/lib/no_brainer/query_runner/database_on_demand.rb +26 -0
  51. data/lib/no_brainer/query_runner/driver.rb +8 -0
  52. data/lib/no_brainer/query_runner/logger.rb +29 -0
  53. data/lib/no_brainer/query_runner/run_options.rb +34 -0
  54. data/lib/no_brainer/query_runner/table_on_demand.rb +44 -0
  55. data/lib/no_brainer/query_runner/write_error.rb +28 -0
  56. data/lib/no_brainer/railtie.rb +36 -0
  57. data/lib/no_brainer/railtie/database.rake +34 -0
  58. data/lib/no_brainer/util.rb +23 -0
  59. data/lib/no_brainer/version.rb +3 -0
  60. data/lib/nobrainer.rb +59 -0
  61. metadata +152 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 572e2605dd72110fd1289cd62b1faa0bf34ae8ef
4
+ data.tar.gz: 55e77059966a3a7cd4d7e95925572304421b20cc
5
+ SHA512:
6
+ metadata.gz: f55312ff9a9617ac05f987a27b4318fe6b039f71e3d52025079c8649d1d41517ed3b3e24023ae29c3f0dc2f16402c9f5d5e5075fdc6553d8184de64c614c0a42
7
+ data.tar.gz: 5337d0ae57e1de9303483df505b02c708ac06e735f44ff794a9c72ac0a8b06e972599ba15735744a82e9286942a9de7ae137c99d6b358c3285bf4f6651a5576d
data/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (C) 2012 Nicolas Viennot
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ NoBrainer
2
+ ===========
3
+
4
+ NoBrainer is a Ruby ORM for [RethinkDB](http://www.rethinkdb.com/).
5
+
6
+ The documentation can be found on [http://nobrainer.io](http://nobrainer.io).
@@ -0,0 +1,14 @@
1
+ module NoBrainer::Autoload
2
+ include ActiveSupport::Autoload
3
+
4
+ def autoload(*args)
5
+ args.each { |mod| super mod }
6
+ end
7
+
8
+ def autoload_and_include(*mods)
9
+ mods.each do |mod|
10
+ autoload mod
11
+ include const_get mod
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ module NoBrainer::Config
2
+ class << self
3
+ mattr_accessor :rethinkdb_url, :logger, :warn_on_active_record,
4
+ :auto_create_databases, :auto_create_tables,
5
+ :cache_documents, :auto_include_timestamps,
6
+ :max_reconnection_tries, :include_root_in_json,
7
+ :durability, :colorize_logger
8
+
9
+ def apply_defaults
10
+ self.rethinkdb_url = default_rethinkdb_url
11
+ self.logger = default_logger
12
+ self.warn_on_active_record = true
13
+ self.auto_create_databases = true
14
+ self.auto_create_tables = true
15
+ self.cache_documents = true
16
+ self.auto_include_timestamps = true
17
+ self.max_reconnection_tries = 10
18
+ self.include_root_in_json = false
19
+ self.durability = default_durability
20
+ self.colorize_logger = true
21
+ end
22
+
23
+ def reset!
24
+ @configured = false
25
+ apply_defaults
26
+ end
27
+
28
+ def configure(&block)
29
+ apply_defaults unless configured?
30
+ block.call(self) if block
31
+ @configured = true
32
+
33
+ NoBrainer.disconnect_if_url_changed
34
+ end
35
+
36
+ def configured?
37
+ !!@configured
38
+ end
39
+
40
+ def default_rethinkdb_url
41
+ return ENV['RETHINKDB_URL'] if ENV['RETHINKDB_URL']
42
+
43
+ if defined?(Rails)
44
+ host = ENV['RETHINKDB_HOST'] || 'localhost'
45
+ port = ENV['RETHINKDB_PORT']
46
+ auth = ENV['RETHINKDB_AUTH']
47
+ db_name = "#{Rails.application.class.parent_name.underscore}_#{Rails.env}"
48
+
49
+ "rethinkdb://#{":#{auth}@" if auth}#{host}#{":#{port}" if port}/#{db_name}"
50
+ end
51
+ end
52
+
53
+ def default_logger
54
+ defined?(Rails) ? Rails.logger : Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
55
+ end
56
+
57
+ def default_durability
58
+ (defined?(Rails) && (Rails.env.test? || Rails.env.development?)) ? :soft : :hard
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,53 @@
1
+ require 'rethinkdb'
2
+
3
+ class NoBrainer::Connection
4
+ # A connection is bound to a specific database.
5
+
6
+ attr_accessor :uri, :host, :port, :database_name, :auth_key
7
+
8
+ def initialize(uri)
9
+ self.uri = uri
10
+ parse_uri
11
+ end
12
+
13
+ def raw
14
+ @raw ||= RethinkDB::Connection.new(:host => host, :port => port, :db => database_name, :auth_key => auth_key)
15
+ end
16
+
17
+ delegate :reconnect, :close, :run, :to => :raw
18
+ alias_method :connect, :raw
19
+ alias_method :disconnect, :close
20
+
21
+ [:db_create, :db_drop, :db_list].each do |cmd|
22
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
+ def #{cmd}(*args)
24
+ NoBrainer.run { |r| r.#{cmd}(*args) }
25
+ end
26
+ RUBY
27
+ end
28
+
29
+ def database
30
+ @database ||= NoBrainer::Database.new(self)
31
+ end
32
+
33
+ private
34
+
35
+ def parse_uri
36
+ require 'uri'
37
+ parsed_uri = URI.parse(uri)
38
+
39
+ if parsed_uri.scheme != 'rethinkdb'
40
+ raise NoBrainer::Error::Connection,
41
+ "Invalid URI. Expecting something like rethinkdb://host:port/database. Got #{uri}"
42
+ end
43
+
44
+ apply_connection_settings!(parsed_uri)
45
+ end
46
+
47
+ def apply_connection_settings!(uri)
48
+ self.host = uri.host
49
+ self.port = uri.port || 28015
50
+ self.database_name = uri.path.gsub(/^\//, '')
51
+ self.auth_key = uri.password
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ require 'rethinkdb'
2
+
3
+ class NoBrainer::Criteria
4
+ # The disctinction between Chainable and Termination is purely cosmetic.
5
+ module Chainable
6
+ extend NoBrainer::Autoload
7
+ extend ActiveSupport::Concern
8
+ autoload_and_include :Core, :Scope, :Raw, :Where, :OrderBy, :Limit
9
+ end
10
+
11
+ module Termination
12
+ extend NoBrainer::Autoload
13
+ extend ActiveSupport::Concern
14
+ autoload_and_include :Count, :Delete, :Enumerable, :First, :EagerLoading,
15
+ :Inc, :Update, :Cache
16
+ end
17
+
18
+ include Chainable
19
+ include Termination
20
+ end
@@ -0,0 +1,67 @@
1
+ module NoBrainer::Criteria::Chainable::Core
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :options }
5
+
6
+ def initialize(options={})
7
+ self.options = options
8
+ end
9
+
10
+ def klass
11
+ options[:klass]
12
+ end
13
+
14
+ def to_rql
15
+ compile_criteria.__send__(:compile_rql)
16
+ end
17
+
18
+ def inspect
19
+ # rescue super because sometimes klass is not set.
20
+ str = to_rql.inspect rescue super
21
+ if str =~ /Erroneous_Portion_Constructed/
22
+ # Need to fix the rethinkdb gem.
23
+ str = "the rethinkdb gem is flipping out with Erroneous_Portion_Constructed"
24
+ end
25
+ str
26
+ end
27
+
28
+ def run(rql=nil)
29
+ NoBrainer.run(rql || to_rql)
30
+ end
31
+
32
+ def merge!(criteria)
33
+ self.options = self.options.merge(criteria.options)
34
+ self
35
+ end
36
+
37
+ def merge(criteria)
38
+ dup.tap { |new_criteria| new_criteria.merge!(criteria) }
39
+ end
40
+
41
+ def ==(other)
42
+ return super if other.is_a?(NoBrainer::Criteria)
43
+ return to_a == other if other.is_a?(Enumerable) && other.first.is_a?(NoBrainer::Document)
44
+ super
45
+ end
46
+
47
+ private
48
+
49
+ def chain(&block)
50
+ tmp = self.class.new(options) # we might want to optimize that thing
51
+ block.call(tmp)
52
+ merge(tmp)
53
+ end
54
+
55
+ def compile_criteria
56
+ # This method is overriden by other modules.
57
+ # compile_criteria returns a criteria that will be used to generate the RQL.
58
+ # This is useful to apply the class default scope at the very end of the chain.
59
+ self
60
+ end
61
+
62
+ def compile_rql
63
+ # This method is overriden by other modules.
64
+ raise "Criteria not bound to a class" unless klass
65
+ klass.rql_table
66
+ end
67
+ end
@@ -0,0 +1,31 @@
1
+ module NoBrainer::Criteria::Chainable::Limit
2
+ # TODO Test these guys
3
+ extend ActiveSupport::Concern
4
+
5
+ included { attr_accessor :_skip, :_limit }
6
+
7
+ def limit(value)
8
+ chain { |criteria| criteria._limit = value }
9
+ end
10
+
11
+ def skip(value)
12
+ chain { |criteria| criteria._skip = value }
13
+ end
14
+ alias_method :offset, :skip
15
+
16
+ def merge!(criteria)
17
+ super
18
+ self._skip = criteria._skip if criteria._skip
19
+ self._limit = criteria._limit if criteria._limit
20
+ self
21
+ end
22
+
23
+ private
24
+
25
+ def compile_rql
26
+ rql = super
27
+ rql = rql.skip(_skip) if _skip
28
+ rql = rql.limit(_limit) if _limit
29
+ rql
30
+ end
31
+ end
@@ -0,0 +1,76 @@
1
+ module NoBrainer::Criteria::Chainable::OrderBy
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :order, :_reverse_order }
5
+
6
+ def initialize(options={})
7
+ super
8
+ self.order = {}
9
+ end
10
+
11
+ def order_by(*rules, &block)
12
+ # Note: We are relying on the fact that Hashes are ordered (since 1.9)
13
+ rules = [*rules, block].compact.map do |rule|
14
+ case rule
15
+ when Hash then
16
+ bad_rule = rule.values.reject { |v| v.in? [:asc, :desc] }.first
17
+ raise_bad_rule(bad_rule) if bad_rule
18
+ rule
19
+ when Symbol then { rule => :asc }
20
+ when Proc then { rule => :asc }
21
+ else raise_bad_rule(rule)
22
+ end
23
+ end.reduce({}, :merge)
24
+
25
+ chain do |criteria|
26
+ criteria.order = rules
27
+ criteria._reverse_order = false
28
+ end
29
+ end
30
+
31
+ def merge!(criteria)
32
+ super
33
+ # The latest order_by() wins
34
+ self.order = criteria.order if criteria.order.present?
35
+ self._reverse_order = criteria._reverse_order unless criteria._reverse_order.nil?
36
+ self
37
+ end
38
+
39
+ def reverse_order
40
+ chain { |criteria| criteria._reverse_order = !self._reverse_order }
41
+ end
42
+
43
+ private
44
+
45
+ def effective_order
46
+ self.order.present? ? self.order : {:id => :asc}
47
+ end
48
+
49
+ def reverse_order?
50
+ !!self._reverse_order
51
+ end
52
+
53
+ def compile_rql
54
+ rql_rules = effective_order.map do |k,v|
55
+ case v
56
+ when :asc then reverse_order? ? RethinkDB::RQL.new.desc(k) : RethinkDB::RQL.new.asc(k)
57
+ when :desc then reverse_order? ? RethinkDB::RQL.new.asc(k) : RethinkDB::RQL.new.desc(k)
58
+ end
59
+ end
60
+
61
+ options = {}
62
+ unless without_index?
63
+ first_key = effective_order.first[0]
64
+ first_key = nil if first_key == :id # FIXME For some reason, using the id index doesn't work.
65
+ if (first_key.is_a?(Symbol) || first_key.is_a?(String)) && klass.has_index?(first_key)
66
+ options[:index] = rql_rules.shift
67
+ end
68
+ end
69
+
70
+ super.order_by(*rql_rules, options)
71
+ end
72
+
73
+ def raise_bad_rule(rule)
74
+ raise "Please pass something like ':field1 => :desc, :field2 => :asc', not #{rule}"
75
+ end
76
+ end
@@ -0,0 +1,25 @@
1
+ module NoBrainer::Criteria::Chainable::Raw
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :_raw }
5
+
6
+ def raw
7
+ chain { |criteria| criteria._raw = true }
8
+ end
9
+
10
+ def merge!(criteria)
11
+ super
12
+ self._raw = criteria._raw unless criteria._raw.nil?
13
+ self
14
+ end
15
+
16
+ private
17
+
18
+ def raw?
19
+ !!_raw
20
+ end
21
+
22
+ def instantiate_doc(attrs)
23
+ raw? ? attrs : klass.new_from_db(attrs)
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ module NoBrainer::Criteria::Chainable::Scope
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :use_default_scope }
5
+
6
+ def scoped
7
+ chain { |criteria| criteria.use_default_scope = true }
8
+ end
9
+
10
+ def unscoped
11
+ chain { |criteria| criteria.use_default_scope = false }
12
+ end
13
+
14
+ def merge!(criteria)
15
+ super
16
+ self.use_default_scope = criteria.use_default_scope unless criteria.use_default_scope.nil?
17
+ self
18
+ end
19
+
20
+ def respond_to?(name, include_private = false)
21
+ super || self.klass.respond_to?(name)
22
+ end
23
+
24
+ def method_missing(name, *args, &block)
25
+ return super unless self.klass.respond_to?(name)
26
+ criteria = self.klass.method(name).call(*args, &block)
27
+ raise "#{name} did not return a criteria" unless criteria.is_a?(NoBrainer::Criteria)
28
+ merge(criteria)
29
+ end
30
+
31
+ private
32
+
33
+ def compile_criteria
34
+ criteria = super
35
+ if klass.default_scope_proc && use_default_scope != false
36
+ criteria = klass.default_scope_proc.call.merge(criteria)
37
+ # XXX If default_scope.class != criteria.class, oops
38
+ end
39
+ criteria
40
+ end
41
+ end
@@ -0,0 +1,198 @@
1
+ module NoBrainer::Criteria::Chainable::Where
2
+ extend ActiveSupport::Concern
3
+
4
+ RESERVED_FIELDS = [:index, :default, :and, :or] + NoBrainer::DecoratedSymbol::MODIFIERS.keys
5
+
6
+ included { attr_accessor :where_ast, :with_index_name }
7
+
8
+ def initialize(options={})
9
+ super
10
+ self.where_ast = MultiOperator.new(:and, [])
11
+ end
12
+
13
+ def where(*args, &block)
14
+ chain { |criteria| criteria.where_ast = parse_clause([*args, block].compact) }
15
+ end
16
+
17
+ def with_index(index_name)
18
+ chain { |criteria| criteria.with_index_name = index_name }
19
+ end
20
+
21
+ def without_index
22
+ with_index(false)
23
+ end
24
+
25
+ def used_index
26
+ IndexFinder.new(compile_criteria).tap { |finder| finder.find_index }.index_name
27
+ end
28
+
29
+ def indexed?
30
+ !!used_index
31
+ end
32
+
33
+ def merge!(criteria)
34
+ super
35
+ clauses = self.where_ast.clauses + criteria.where_ast.clauses
36
+ self.where_ast = MultiOperator.new(:and, clauses).simplify
37
+ self.with_index_name = criteria.with_index_name unless criteria.with_index_name.nil?
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ class MultiOperator < Struct.new(:op, :clauses)
44
+ def simplify
45
+ same_op_clauses, other_clauses = self.clauses.map(&:simplify)
46
+ .partition { |v| v.is_a?(MultiOperator) && self.op == v.op }
47
+ simplified_clauses = other_clauses + same_op_clauses.map(&:clauses).flatten(1)
48
+ MultiOperator.new(op, simplified_clauses.uniq)
49
+ end
50
+
51
+ def to_rql(doc)
52
+ case op
53
+ when :and then clauses.map { |c| c.to_rql(doc) }.reduce { |a,b| a & b }
54
+ when :or then clauses.map { |c| c.to_rql(doc) }.reduce { |a,b| a | b }
55
+ end
56
+ end
57
+ end
58
+
59
+ class BinaryOperator < Struct.new(:key, :op, :value)
60
+ def simplify
61
+ self
62
+ end
63
+
64
+ def to_rql(doc)
65
+ case op
66
+ when :between then (doc[key] >= value.min) & (doc[key] <= value.max)
67
+ when :in then value.map { |v| doc[key].eq(v) }.reduce { |a,b| a | b }
68
+ else doc[key].__send__(op, value)
69
+ end
70
+ end
71
+ end
72
+
73
+ class UnaryOperator < Struct.new(:op, :value)
74
+ def simplify
75
+ value.is_a?(UnaryOperator) && [self.op, value.op] == [:not, :not] ? value.value : self
76
+ end
77
+
78
+ def to_rql(doc)
79
+ case op
80
+ when :not then value.to_rql(doc).not
81
+ end
82
+ end
83
+ end
84
+
85
+ class Lambda < Struct.new(:value)
86
+ def simplify
87
+ self
88
+ end
89
+
90
+ def to_rql(doc)
91
+ value.call(doc)
92
+ end
93
+ end
94
+
95
+ def parse_clause(clause)
96
+ case clause
97
+ when Array then MultiOperator.new(:and, clause.map { |c| parse_clause(c) })
98
+ when Hash then MultiOperator.new(:and, clause.map { |k,v| parse_clause_stub(k,v) })
99
+ when Proc then Lambda.new(clause)
100
+ when NoBrainer::DecoratedSymbol
101
+ case clause.args.size
102
+ when 1 then parse_clause_stub(clause, clause.args.first)
103
+ else raise "Invalid argument: #{clause}"
104
+ end
105
+ else raise "Invalid clause: #{clause}"
106
+ end
107
+ end
108
+
109
+ def parse_clause_stub(key, value)
110
+ case key
111
+ when :and then MultiOperator.new(:and, value.map { |v| parse_clause(v) })
112
+ when :or then MultiOperator.new(:or, value.map { |v| parse_clause(v) })
113
+ when :not then UnaryOperator.new(:not, parse_clause(value))
114
+ when String, Symbol then parse_clause_stub(key.to_sym.eq, value)
115
+ when NoBrainer::DecoratedSymbol then
116
+ case key.modifier
117
+ when :ne then parse_clause(:not => { key.symbol => value })
118
+ when :eq then
119
+ case value
120
+ when Range then BinaryOperator.new(key.symbol, :between, value)
121
+ when Regexp then BinaryOperator.new(key.symbol, :match, value.inspect[1..-2])
122
+ else BinaryOperator.new(key.symbol, key.modifier, value)
123
+ end
124
+ else BinaryOperator.new(key.symbol, key.modifier, value)
125
+ end
126
+ else raise "Invalid key: #{key}"
127
+ end
128
+ end
129
+
130
+ def without_index?
131
+ self.with_index_name == false
132
+ end
133
+
134
+ class IndexFinder < Struct.new(:criteria, :index_name, :indexed_values, :ast)
135
+ def get_candidate_clauses(*types)
136
+ Hash[criteria.where_ast.clauses
137
+ .select { |c| c.is_a?(BinaryOperator) && types.include?(c.op) }
138
+ .map { |c| [c.key, c] }]
139
+ end
140
+
141
+ def get_usable_indexes(*types)
142
+ indexes = criteria.klass.indexes
143
+ indexes = indexes.select { |k,v| types.include?(v[:kind]) } if types.present?
144
+ indexes = indexes.select { |k,v| k == criteria.with_index_name.to_sym } if criteria.with_index_name
145
+ indexes
146
+ end
147
+
148
+ def find_index_canonical
149
+ clauses = get_candidate_clauses(:eq, :in)
150
+ return unless clauses.present?
151
+
152
+ if index_name = (get_usable_indexes.keys & clauses.keys).first
153
+ clause = clauses[index_name]
154
+ self.index_name = index_name
155
+ self.indexed_values = clause.op == :in ? clause.value : [clause.value]
156
+ self.ast = MultiOperator.new(:and, criteria.where_ast.clauses - [clause])
157
+ end
158
+ end
159
+
160
+ def find_index_compound
161
+ clauses = get_candidate_clauses(:eq)
162
+ return unless clauses.present?
163
+
164
+ index_name, index_values = get_usable_indexes(:compound)
165
+ .map { |name, option| [name, option[:what]] }
166
+ .select { |name, values| values & clauses.keys == values }
167
+ .first
168
+
169
+ if index_name
170
+ indexed_clauses = index_values.map { |field| clauses[field] }
171
+ self.index_name = index_name
172
+ self.indexed_values = [indexed_clauses.map { |c| c.value }]
173
+ self.ast = MultiOperator.new(:and, criteria.where_ast.clauses - indexed_clauses)
174
+ end
175
+ end
176
+
177
+ def find_index
178
+ return false if criteria.__send__(:without_index?)
179
+ could_find_index = find_index_canonical || find_index_compound
180
+ if criteria.with_index_name && !could_find_index
181
+ raise NoBrainer::Error::CannotUseIndex.new("Cannot use index #{criteria.with_index_name}")
182
+ end
183
+ !!could_find_index
184
+ end
185
+ end
186
+
187
+ def compile_rql
188
+ rql = super
189
+ ast = self.where_ast
190
+ finder = IndexFinder.new(self)
191
+ if finder.find_index
192
+ ast = finder.ast
193
+ rql = rql.get_all(*finder.indexed_values, :index => finder.index_name)
194
+ end
195
+ rql = rql.filter { |doc| ast.to_rql(doc) } if ast.clauses.present?
196
+ rql
197
+ end
198
+ end