nobrainer 0.8.0 → 0.9.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.
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