nobrainer 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +9 -16
  3. data/lib/no_brainer/connection.rb +32 -26
  4. data/lib/no_brainer/criteria/chainable/core.rb +17 -20
  5. data/lib/no_brainer/criteria/chainable/limit.rb +2 -2
  6. data/lib/no_brainer/criteria/chainable/order_by.rb +24 -7
  7. data/lib/no_brainer/criteria/chainable/raw.rb +11 -3
  8. data/lib/no_brainer/criteria/chainable/scope.rb +11 -7
  9. data/lib/no_brainer/criteria/chainable/where.rb +32 -15
  10. data/lib/no_brainer/criteria/termination/cache.rb +13 -9
  11. data/lib/no_brainer/criteria/termination/eager_loading.rb +12 -26
  12. data/lib/no_brainer/document.rb +2 -6
  13. data/lib/no_brainer/document/association.rb +8 -7
  14. data/lib/no_brainer/document/association/belongs_to.rb +27 -22
  15. data/lib/no_brainer/document/association/core.rb +8 -8
  16. data/lib/no_brainer/document/association/eager_loader.rb +57 -0
  17. data/lib/no_brainer/document/association/has_many.rb +49 -26
  18. data/lib/no_brainer/document/association/has_many_through.rb +31 -0
  19. data/lib/no_brainer/document/association/has_one.rb +13 -0
  20. data/lib/no_brainer/document/association/has_one_through.rb +10 -0
  21. data/lib/no_brainer/document/attributes.rb +4 -3
  22. data/lib/no_brainer/document/core.rb +1 -1
  23. data/lib/no_brainer/document/criteria.rb +2 -3
  24. data/lib/no_brainer/document/index.rb +3 -3
  25. data/lib/no_brainer/document/polymorphic.rb +1 -1
  26. data/lib/no_brainer/document/serialization.rb +1 -1
  27. data/lib/no_brainer/document/store_in.rb +5 -3
  28. data/lib/no_brainer/error.rb +1 -0
  29. data/lib/no_brainer/query_runner.rb +2 -1
  30. data/lib/no_brainer/query_runner/driver.rb +1 -4
  31. data/lib/no_brainer/query_runner/missing_index.rb +11 -0
  32. data/lib/no_brainer/query_runner/run_options.rb +2 -3
  33. data/lib/no_brainer/railtie.rb +0 -1
  34. data/lib/no_brainer/version.rb +1 -1
  35. data/lib/nobrainer.rb +5 -5
  36. metadata +28 -24
  37. data/lib/no_brainer/database.rb +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 572e2605dd72110fd1289cd62b1faa0bf34ae8ef
4
- data.tar.gz: 55e77059966a3a7cd4d7e95925572304421b20cc
3
+ metadata.gz: ea32475119089388850bb55326aab70d4a3dbc16
4
+ data.tar.gz: dfbef7c1a239b75cb3b45d32b593026ef4d8df93
5
5
  SHA512:
6
- metadata.gz: f55312ff9a9617ac05f987a27b4318fe6b039f71e3d52025079c8649d1d41517ed3b3e24023ae29c3f0dc2f16402c9f5d5e5075fdc6553d8184de64c614c0a42
7
- data.tar.gz: 5337d0ae57e1de9303483df505b02c708ac06e735f44ff794a9c72ac0a8b06e972599ba15735744a82e9286942a9de7ae137c99d6b358c3285bf4f6651a5576d
6
+ metadata.gz: d83f20ced889fba1ba1048e6ad41f4c7a6a1ae02562c8bf230580a66e64306d9a064eb179a500b61b230f026bbc93a8bf8876f4b4edbf2e21c1a2140decb743d
7
+ data.tar.gz: a9a671db853f43d369550c7b1031a3a9ad17b9d5307aee6aea659e3808364c6d3d77b0ecfe9d2354a08efb99df7f44f085566d36d2f787a3e1d7469ec915ba52
@@ -2,9 +2,7 @@ module NoBrainer::Config
2
2
  class << self
3
3
  mattr_accessor :rethinkdb_url, :logger, :warn_on_active_record,
4
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
5
+ :max_reconnection_tries, :durability, :colorize_logger
8
6
 
9
7
  def apply_defaults
10
8
  self.rethinkdb_url = default_rethinkdb_url
@@ -12,10 +10,7 @@ module NoBrainer::Config
12
10
  self.warn_on_active_record = true
13
11
  self.auto_create_databases = true
14
12
  self.auto_create_tables = true
15
- self.cache_documents = true
16
- self.auto_include_timestamps = true
17
13
  self.max_reconnection_tries = 10
18
- self.include_root_in_json = false
19
14
  self.durability = default_durability
20
15
  self.colorize_logger = true
21
16
  end
@@ -38,16 +33,14 @@ module NoBrainer::Config
38
33
  end
39
34
 
40
35
  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
36
+ db = ENV['RETHINKDB_DB'] || ENV['RDB_DB']
37
+ db ||= "#{Rails.application.class.parent_name.underscore}_#{Rails.env}" if defined?(Rails)
38
+ host = ENV['RETHINKDB_HOST'] || ENV['RDB_HOST'] || 'localhost'
39
+ port = ENV['RETHINKDB_PORT'] || ENV['RDB_PORT']
40
+ auth = ENV['RETHINKDB_AUTH'] || ENV['RDB_AUTH']
41
+ url = ENV['RETHINKDB_URL'] || ENV['RDB_URL']
42
+ url ||= "rethinkdb://#{":#{auth}@" if auth}#{host}#{":#{port}" if port}/#{db}" if db
43
+ url
51
44
  end
52
45
 
53
46
  def default_logger
@@ -1,24 +1,38 @@
1
1
  require 'rethinkdb'
2
2
 
3
3
  class NoBrainer::Connection
4
- # A connection is bound to a specific database.
5
-
6
- attr_accessor :uri, :host, :port, :database_name, :auth_key
4
+ attr_accessor :uri
7
5
 
8
6
  def initialize(uri)
9
7
  self.uri = uri
10
- parse_uri
8
+ parsed_uri # just to raise an exception if there is a problem.
9
+ end
10
+
11
+ def parsed_uri
12
+ require 'uri'
13
+ uri = URI.parse(self.uri)
14
+
15
+ if uri.scheme != 'rethinkdb'
16
+ raise NoBrainer::Error::Connection,
17
+ "Invalid URI. Expecting something like rethinkdb://host:port/database. Got #{uri}"
18
+ end
19
+
20
+ { :auth_key => uri.password,
21
+ :host => uri.host,
22
+ :port => uri.port || 28015,
23
+ :db => uri.path.gsub(/^\//, ''),
24
+ }.tap { |result| raise "No database specified in #{uri}" unless result[:db].present? }
11
25
  end
12
26
 
13
27
  def raw
14
- @raw ||= RethinkDB::Connection.new(:host => host, :port => port, :db => database_name, :auth_key => auth_key)
28
+ @raw ||= RethinkDB::Connection.new(parsed_uri)
15
29
  end
16
30
 
17
31
  delegate :reconnect, :close, :run, :to => :raw
18
32
  alias_method :connect, :raw
19
33
  alias_method :disconnect, :close
20
34
 
21
- [:db_create, :db_drop, :db_list].each do |cmd|
35
+ [:db_create, :db_drop, :db_list, :table_create, :table_drop, :table_list].each do |cmd|
22
36
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
37
  def #{cmd}(*args)
24
38
  NoBrainer.run { |r| r.#{cmd}(*args) }
@@ -26,28 +40,20 @@ class NoBrainer::Connection
26
40
  RUBY
27
41
  end
28
42
 
29
- def database
30
- @database ||= NoBrainer::Database.new(self)
43
+ def drop!
44
+ # XXX Thread.current[:nobrainer_options] is set by NoBrainer::QueryRunner::RunOptions
45
+ db = (Thread.current[:nobrainer_options] || parsed_uri)[:db]
46
+ db_drop(db)['dropped'] == 1
31
47
  end
32
48
 
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}"
49
+ # Note that truncating each table (purge) is much faster than dropping the
50
+ # database (drop)
51
+ def purge!(options={})
52
+ table_list.each do |table_name|
53
+ NoBrainer.run { |r| r.table(table_name).delete }
42
54
  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
55
+ true
56
+ rescue RuntimeError => e
57
+ raise e unless e.message =~ /No entry with that name/
52
58
  end
53
59
  end
@@ -1,18 +1,18 @@
1
1
  module NoBrainer::Criteria::Chainable::Core
2
2
  extend ActiveSupport::Concern
3
3
 
4
- included { attr_accessor :options }
4
+ included { attr_accessor :init_options }
5
5
 
6
6
  def initialize(options={})
7
- self.options = options
7
+ self.init_options = options
8
8
  end
9
9
 
10
10
  def klass
11
- options[:klass]
11
+ init_options[:klass]
12
12
  end
13
13
 
14
14
  def to_rql
15
- compile_criteria.__send__(:compile_rql)
15
+ with_default_scope_applied.__send__(:compile_rql_pass2)
16
16
  end
17
17
 
18
18
  def inspect
@@ -29,39 +29,36 @@ module NoBrainer::Criteria::Chainable::Core
29
29
  NoBrainer.run(rql || to_rql)
30
30
  end
31
31
 
32
- def merge!(criteria)
33
- self.options = self.options.merge(criteria.options)
32
+ def merge!(criteria, options={})
33
+ self.init_options = self.init_options.merge(criteria.init_options)
34
34
  self
35
35
  end
36
36
 
37
- def merge(criteria)
38
- dup.tap { |new_criteria| new_criteria.merge!(criteria) }
37
+ def merge(criteria, options={})
38
+ dup.tap { |new_criteria| new_criteria.merge!(criteria, options) }
39
39
  end
40
40
 
41
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)
42
+ return to_a == other if other.is_a?(Array)
44
43
  super
45
44
  end
46
45
 
47
46
  private
48
47
 
49
- def chain(&block)
50
- tmp = self.class.new(options) # we might want to optimize that thing
48
+ def chain(options={}, &block)
49
+ tmp = self.class.new(self.init_options) # we might want to optimize that thing
51
50
  block.call(tmp)
52
- merge(tmp)
51
+ merge(tmp, options)
53
52
  end
54
53
 
55
- def compile_criteria
54
+ def compile_rql_pass1
56
55
  # 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
56
+ raise "Criteria not bound to a class" unless klass
57
+ klass.rql_table
60
58
  end
61
59
 
62
- def compile_rql
60
+ def compile_rql_pass2
63
61
  # This method is overriden by other modules.
64
- raise "Criteria not bound to a class" unless klass
65
- klass.rql_table
62
+ compile_rql_pass1
66
63
  end
67
64
  end
@@ -13,7 +13,7 @@ module NoBrainer::Criteria::Chainable::Limit
13
13
  end
14
14
  alias_method :offset, :skip
15
15
 
16
- def merge!(criteria)
16
+ def merge!(criteria, options={})
17
17
  super
18
18
  self._skip = criteria._skip if criteria._skip
19
19
  self._limit = criteria._limit if criteria._limit
@@ -22,7 +22,7 @@ module NoBrainer::Criteria::Chainable::Limit
22
22
 
23
23
  private
24
24
 
25
- def compile_rql
25
+ def compile_rql_pass2
26
26
  rql = super
27
27
  rql = rql.skip(_skip) if _skip
28
28
  rql = rql.limit(_limit) if _limit
@@ -28,7 +28,7 @@ module NoBrainer::Criteria::Chainable::OrderBy
28
28
  end
29
29
  end
30
30
 
31
- def merge!(criteria)
31
+ def merge!(criteria, options={})
32
32
  super
33
33
  # The latest order_by() wins
34
34
  self.order = criteria.order if criteria.order.present?
@@ -43,14 +43,16 @@ module NoBrainer::Criteria::Chainable::OrderBy
43
43
  private
44
44
 
45
45
  def effective_order
46
- self.order.present? ? self.order : {:id => :asc}
46
+ self.order.presence || {:id => :asc}
47
47
  end
48
48
 
49
49
  def reverse_order?
50
50
  !!self._reverse_order
51
51
  end
52
52
 
53
- def compile_rql
53
+ def compile_rql_pass1
54
+ rql = super
55
+
54
56
  rql_rules = effective_order.map do |k,v|
55
57
  case v
56
58
  when :asc then reverse_order? ? RethinkDB::RQL.new.desc(k) : RethinkDB::RQL.new.asc(k)
@@ -58,16 +60,31 @@ module NoBrainer::Criteria::Chainable::OrderBy
58
60
  end
59
61
  end
60
62
 
61
- options = {}
62
- unless without_index?
63
+ # We can only apply an index order_by on a table() term.
64
+ # We are going to try to go so and if we cannot, we'll simply apply
65
+ # the ordering in pass2, which will happen after a potential filter().
66
+
67
+ if rql.body.type == Term::TermType::TABLE && !without_index?
68
+ options = {}
63
69
  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
70
  if (first_key.is_a?(Symbol) || first_key.is_a?(String)) && klass.has_index?(first_key)
66
71
  options[:index] = rql_rules.shift
67
72
  end
73
+
74
+ rql = rql.order_by(*rql_rules, options)
75
+ else
76
+ # Stashing @rql_rules for pass2, which is a pretty gross hack.
77
+ # We should really use more of a middleware pattern to build the RQL.
78
+ @rql_rules_pass2 = rql_rules
68
79
  end
69
80
 
70
- super.order_by(*rql_rules, options)
81
+ rql
82
+ end
83
+
84
+ def compile_rql_pass2
85
+ rql = super
86
+ rql = rql.order_by(*@rql_rules_pass2) if @rql_rules_pass2
87
+ rql
71
88
  end
72
89
 
73
90
  def raise_bad_rule(rule)
@@ -1,15 +1,22 @@
1
1
  module NoBrainer::Criteria::Chainable::Raw
2
2
  extend ActiveSupport::Concern
3
3
 
4
- included { attr_accessor :_raw }
4
+ included { attr_accessor :_raw, :__after_instantiate }
5
5
 
6
6
  def raw
7
7
  chain { |criteria| criteria._raw = true }
8
8
  end
9
9
 
10
- def merge!(criteria)
10
+ def _after_instantiate(block)
11
+ # Just some helper for the has_many association to set the inverses on the
12
+ # related belongs_to. A bit hackish.
13
+ chain { |criteria| criteria.__after_instantiate = block }
14
+ end
15
+
16
+ def merge!(criteria, options={})
11
17
  super
12
18
  self._raw = criteria._raw unless criteria._raw.nil?
19
+ self.__after_instantiate ||= criteria.__after_instantiate
13
20
  self
14
21
  end
15
22
 
@@ -20,6 +27,7 @@ module NoBrainer::Criteria::Chainable::Raw
20
27
  end
21
28
 
22
29
  def instantiate_doc(attrs)
23
- raw? ? attrs : klass.new_from_db(attrs)
30
+ (raw? ? attrs : klass.new_from_db(attrs))
31
+ .tap { |doc| __after_instantiate.call(doc) if __after_instantiate }
24
32
  end
25
33
  end
@@ -11,7 +11,7 @@ module NoBrainer::Criteria::Chainable::Scope
11
11
  chain { |criteria| criteria.use_default_scope = false }
12
12
  end
13
13
 
14
- def merge!(criteria)
14
+ def merge!(criteria, options={})
15
15
  super
16
16
  self.use_default_scope = criteria.use_default_scope unless criteria.use_default_scope.nil?
17
17
  self
@@ -30,12 +30,16 @@ module NoBrainer::Criteria::Chainable::Scope
30
30
 
31
31
  private
32
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
33
+ def should_apply_default_scope?
34
+ klass.default_scope_proc && use_default_scope != false
35
+ end
36
+
37
+ def with_default_scope_applied
38
+ if should_apply_default_scope?
39
+ # XXX If default_scope.class != self.class, oops
40
+ klass.default_scope_proc.call.merge(self).unscoped
41
+ else
42
+ self
38
43
  end
39
- criteria
40
44
  end
41
45
  end
@@ -1,8 +1,6 @@
1
1
  module NoBrainer::Criteria::Chainable::Where
2
2
  extend ActiveSupport::Concern
3
3
 
4
- RESERVED_FIELDS = [:index, :default, :and, :or] + NoBrainer::DecoratedSymbol::MODIFIERS.keys
5
-
6
4
  included { attr_accessor :where_ast, :with_index_name }
7
5
 
8
6
  def initialize(options={})
@@ -23,14 +21,14 @@ module NoBrainer::Criteria::Chainable::Where
23
21
  end
24
22
 
25
23
  def used_index
26
- IndexFinder.new(compile_criteria).tap { |finder| finder.find_index }.index_name
24
+ index_finder.index_name
27
25
  end
28
26
 
29
27
  def indexed?
30
- !!used_index
28
+ index_finder.could_find_index?
31
29
  end
32
30
 
33
- def merge!(criteria)
31
+ def merge!(criteria, options={})
34
32
  super
35
33
  clauses = self.where_ast.clauses + criteria.where_ast.clauses
36
34
  self.where_ast = MultiOperator.new(:and, clauses).simplify
@@ -58,6 +56,7 @@ module NoBrainer::Criteria::Chainable::Where
58
56
 
59
57
  class BinaryOperator < Struct.new(:key, :op, :value)
60
58
  def simplify
59
+ # TODO Simplify the in uniq
61
60
  self
62
61
  end
63
62
 
@@ -132,6 +131,17 @@ module NoBrainer::Criteria::Chainable::Where
132
131
  end
133
132
 
134
133
  class IndexFinder < Struct.new(:criteria, :index_name, :indexed_values, :ast)
134
+ def initialize(*args)
135
+ super
136
+ find_index
137
+ end
138
+
139
+ def could_find_index?
140
+ !!self.index_name
141
+ end
142
+
143
+ private
144
+
135
145
  def get_candidate_clauses(*types)
136
146
  Hash[criteria.where_ast.clauses
137
147
  .select { |c| c.is_a?(BinaryOperator) && types.include?(c.op) }
@@ -175,23 +185,30 @@ module NoBrainer::Criteria::Chainable::Where
175
185
  end
176
186
 
177
187
  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
188
+ return if criteria.__send__(:without_index?)
189
+ find_index_canonical || find_index_compound
190
+ if criteria.with_index_name && !could_find_index?
181
191
  raise NoBrainer::Error::CannotUseIndex.new("Cannot use index #{criteria.with_index_name}")
182
192
  end
183
- !!could_find_index
184
193
  end
185
194
  end
186
195
 
187
- def compile_rql
196
+ def index_finder
197
+ return with_default_scope_applied.__send__(:index_finder) if should_apply_default_scope?
198
+ @index_finder ||= IndexFinder.new(self)
199
+ end
200
+
201
+ def compile_rql_pass1
188
202
  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)
203
+ if index_finder.could_find_index?
204
+ rql = rql.get_all(*index_finder.indexed_values, :index => index_finder.index_name)
194
205
  end
206
+ rql
207
+ end
208
+
209
+ def compile_rql_pass2
210
+ rql = super
211
+ ast = index_finder.could_find_index? ? index_finder.ast : self.where_ast
195
212
  rql = rql.filter { |doc| ast.to_rql(doc) } if ast.clauses.present?
196
213
  rql
197
214
  end