nobrainer 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +7 -0
- data/README.md +6 -0
- data/lib/no_brainer/autoload.rb +14 -0
- data/lib/no_brainer/config.rb +61 -0
- data/lib/no_brainer/connection.rb +53 -0
- data/lib/no_brainer/criteria.rb +20 -0
- data/lib/no_brainer/criteria/chainable/core.rb +67 -0
- data/lib/no_brainer/criteria/chainable/limit.rb +31 -0
- data/lib/no_brainer/criteria/chainable/order_by.rb +76 -0
- data/lib/no_brainer/criteria/chainable/raw.rb +25 -0
- data/lib/no_brainer/criteria/chainable/scope.rb +41 -0
- data/lib/no_brainer/criteria/chainable/where.rb +198 -0
- data/lib/no_brainer/criteria/termination/cache.rb +71 -0
- data/lib/no_brainer/criteria/termination/count.rb +19 -0
- data/lib/no_brainer/criteria/termination/delete.rb +11 -0
- data/lib/no_brainer/criteria/termination/eager_loading.rb +64 -0
- data/lib/no_brainer/criteria/termination/enumerable.rb +24 -0
- data/lib/no_brainer/criteria/termination/first.rb +25 -0
- data/lib/no_brainer/criteria/termination/inc.rb +14 -0
- data/lib/no_brainer/criteria/termination/update.rb +13 -0
- data/lib/no_brainer/database.rb +41 -0
- data/lib/no_brainer/decorated_symbol.rb +15 -0
- data/lib/no_brainer/document.rb +18 -0
- data/lib/no_brainer/document/association.rb +41 -0
- data/lib/no_brainer/document/association/belongs_to.rb +64 -0
- data/lib/no_brainer/document/association/core.rb +64 -0
- data/lib/no_brainer/document/association/has_many.rb +68 -0
- data/lib/no_brainer/document/attributes.rb +124 -0
- data/lib/no_brainer/document/core.rb +20 -0
- data/lib/no_brainer/document/criteria.rb +62 -0
- data/lib/no_brainer/document/dirty.rb +88 -0
- data/lib/no_brainer/document/dynamic_attributes.rb +12 -0
- data/lib/no_brainer/document/id.rb +49 -0
- data/lib/no_brainer/document/index.rb +102 -0
- data/lib/no_brainer/document/injection_layer.rb +12 -0
- data/lib/no_brainer/document/persistance.rb +124 -0
- data/lib/no_brainer/document/polymorphic.rb +43 -0
- data/lib/no_brainer/document/serialization.rb +9 -0
- data/lib/no_brainer/document/store_in.rb +33 -0
- data/lib/no_brainer/document/timestamps.rb +18 -0
- data/lib/no_brainer/document/validation.rb +35 -0
- data/lib/no_brainer/error.rb +10 -0
- data/lib/no_brainer/fork.rb +14 -0
- data/lib/no_brainer/index_manager.rb +6 -0
- data/lib/no_brainer/loader.rb +5 -0
- data/lib/no_brainer/locale/en.yml +4 -0
- data/lib/no_brainer/query_runner.rb +37 -0
- data/lib/no_brainer/query_runner/connection.rb +17 -0
- data/lib/no_brainer/query_runner/database_on_demand.rb +26 -0
- data/lib/no_brainer/query_runner/driver.rb +8 -0
- data/lib/no_brainer/query_runner/logger.rb +29 -0
- data/lib/no_brainer/query_runner/run_options.rb +34 -0
- data/lib/no_brainer/query_runner/table_on_demand.rb +44 -0
- data/lib/no_brainer/query_runner/write_error.rb +28 -0
- data/lib/no_brainer/railtie.rb +36 -0
- data/lib/no_brainer/railtie/database.rake +34 -0
- data/lib/no_brainer/util.rb +23 -0
- data/lib/no_brainer/version.rb +3 -0
- data/lib/nobrainer.rb +59 -0
- 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,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
|