nobrainer 0.13.1 → 0.14.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +3 -3
  3. data/lib/no_brainer/criteria/delete.rb +2 -2
  4. data/lib/no_brainer/criteria/order_by.rb +33 -10
  5. data/lib/no_brainer/criteria/update.rb +2 -2
  6. data/lib/no_brainer/criteria/where.rb +1 -1
  7. data/lib/no_brainer/document/association/belongs_to.rb +10 -5
  8. data/lib/no_brainer/document/association/core.rb +1 -1
  9. data/lib/no_brainer/document/association/has_many.rb +10 -4
  10. data/lib/no_brainer/document/attributes.rb +27 -3
  11. data/lib/no_brainer/document/callbacks.rb +2 -10
  12. data/lib/no_brainer/document/core.rb +5 -3
  13. data/lib/no_brainer/document/criteria.rb +9 -9
  14. data/lib/no_brainer/document/dirty.rb +11 -0
  15. data/lib/no_brainer/document/dynamic_attributes.rb +4 -0
  16. data/lib/no_brainer/document/id.rb +49 -4
  17. data/lib/no_brainer/document/index.rb +6 -2
  18. data/lib/no_brainer/document/persistance.rb +5 -4
  19. data/lib/no_brainer/document/readonly.rb +7 -0
  20. data/lib/no_brainer/document/serialization.rb +4 -0
  21. data/lib/no_brainer/document/types.rb +8 -0
  22. data/lib/no_brainer/document/uniqueness.rb +4 -2
  23. data/lib/no_brainer/document/validation.rb +1 -1
  24. data/lib/no_brainer/document.rb +2 -0
  25. data/lib/no_brainer/error.rb +8 -8
  26. data/lib/no_brainer/query_runner/logger.rb +18 -12
  27. data/lib/no_brainer/query_runner/missing_index.rb +15 -3
  28. data/lib/no_brainer/query_runner/reconnect.rb +15 -4
  29. data/lib/no_brainer/query_runner/table_on_demand.rb +14 -25
  30. data/lib/no_brainer/query_runner/write_error.rb +5 -8
  31. data/lib/no_brainer/rql.rb +25 -0
  32. data/lib/nobrainer.rb +5 -6
  33. metadata +11 -11
  34. data/lib/no_brainer/util.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b5ca498dfa914c0c0178f339485d78ba804c5b95
4
- data.tar.gz: c0e0c6f35d8927bb6281f860aac769753df59b88
3
+ metadata.gz: 43783bcf0d625af679de685ab771782ce9b141f3
4
+ data.tar.gz: 4b5e4ff205f55adb754133b681b6e71c71772790
5
5
  SHA512:
6
- metadata.gz: ed914e1eee020658197d596245404a2a088acbf033f5fc3a44cb490620cdc5ef110d62e4da927e7579e5e3383bc90e16dd499269a9bd8952dfa2011d3e2f2a12
7
- data.tar.gz: f3204081b4ef7617706ee3c037830bdacbde973ae79e914b782b39fb41f1ec36b0125f78b427d0f21078bdae543ee426f2936ba69a1327bc36176acc6997765e
6
+ metadata.gz: 1c182d056390dd2c7b51bac68999d79c72cfaca49837239c4eead56ce8d0ff474630cc7b24e9fa12a4ff8d36641a5f6cc46fb7c588c45e975af42e8d4b5dbebc
7
+ data.tar.gz: 5226adb01498de4e82f8b3a4729c818ed1a40dc07e3596061d64956407b9a62129108046e0625bbe60bcac1804f0b8111cab4ec3a736151b6cfdc3d5799f2d88
@@ -38,7 +38,7 @@ module NoBrainer::Config
38
38
 
39
39
  def default_rethinkdb_url
40
40
  db = ENV['RETHINKDB_DB'] || ENV['RDB_DB']
41
- db ||= "#{Rails.application.class.parent_name.underscore}_#{Rails.env}" if defined?(Rails)
41
+ db ||= "#{Rails.application.class.parent_name.underscore}_#{Rails.env}" rescue nil
42
42
  host = ENV['RETHINKDB_HOST'] || ENV['RDB_HOST'] || 'localhost'
43
43
  port = ENV['RETHINKDB_PORT'] || ENV['RDB_PORT']
44
44
  auth = ENV['RETHINKDB_AUTH'] || ENV['RDB_AUTH']
@@ -48,11 +48,11 @@ module NoBrainer::Config
48
48
  end
49
49
 
50
50
  def default_logger
51
- defined?(Rails) ? Rails.logger : Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
51
+ defined?(Rails.logger) ? Rails.logger : Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
52
52
  end
53
53
 
54
54
  def default_durability
55
- (defined?(Rails) && (Rails.env.test? || Rails.env.development?)) ? :soft : :hard
55
+ (defined?(Rails.env) && (Rails.env.test? || Rails.env.development?)) ? :soft : :hard
56
56
  end
57
57
  end
58
58
  end
@@ -2,10 +2,10 @@ module NoBrainer::Criteria::Delete
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def delete_all
5
- run(to_rql.delete)
5
+ run(without_ordering.to_rql.delete)
6
6
  end
7
7
 
8
8
  def destroy_all
9
- to_a.each { |doc| doc.destroy }
9
+ without_ordering.to_a.each { |doc| doc.destroy }
10
10
  end
11
11
  end
@@ -1,7 +1,7 @@
1
1
  module NoBrainer::Criteria::OrderBy
2
2
  extend ActiveSupport::Concern
3
3
 
4
- included { attr_accessor :order, :_reverse_order }
4
+ included { attr_accessor :order, :ordering_mode }
5
5
 
6
6
  def initialize(options={})
7
7
  super
@@ -24,36 +24,55 @@ module NoBrainer::Criteria::OrderBy
24
24
 
25
25
  chain do |criteria|
26
26
  criteria.order = rules
27
- criteria._reverse_order = false
27
+ criteria.ordering_mode = :normal
28
28
  end
29
29
  end
30
30
 
31
+ def without_ordering
32
+ chain { |criteria| criteria.ordering_mode = :disabled }
33
+ end
34
+
31
35
  def merge!(criteria, options={})
32
36
  super
33
37
  # The latest order_by() wins
34
38
  self.order = criteria.order if criteria.order.present?
35
- self._reverse_order = criteria._reverse_order unless criteria._reverse_order.nil?
39
+ self.ordering_mode = criteria.ordering_mode unless criteria.ordering_mode.nil?
36
40
  self
37
41
  end
38
42
 
39
43
  def reverse_order
40
- chain { |criteria| criteria._reverse_order = !self._reverse_order }
44
+ chain do |criteria|
45
+ criteria.ordering_mode =
46
+ case self.ordering_mode
47
+ when nil then :reversed
48
+ when :normal then :reversed
49
+ when :reversed then :normal
50
+ when :disabled then :disabled
51
+ end
52
+ end
41
53
  end
42
54
 
43
55
  private
44
56
 
45
57
  def effective_order
46
- self.order.presence || {:id => :asc}
58
+ self.order.presence || (klass ? {klass.pk_name => :asc} : {})
47
59
  end
48
60
 
49
61
  def reverse_order?
50
- !!self._reverse_order
62
+ self.ordering_mode == :reversed
63
+ end
64
+
65
+ def should_order?
66
+ self.ordering_mode != :disabled
51
67
  end
52
68
 
53
69
  def compile_rql_pass1
54
70
  rql = super
71
+ return rql unless should_order?
72
+ _effective_order = effective_order
73
+ return rql if _effective_order.empty?
55
74
 
56
- rql_rules = effective_order.map do |k,v|
75
+ rql_rules = _effective_order.map do |k,v|
57
76
  case v
58
77
  when :asc then reverse_order? ? RethinkDB::RQL.new.desc(k) : RethinkDB::RQL.new.asc(k)
59
78
  when :desc then reverse_order? ? RethinkDB::RQL.new.asc(k) : RethinkDB::RQL.new.desc(k)
@@ -64,9 +83,10 @@ module NoBrainer::Criteria::OrderBy
64
83
  # We are going to try to go so and if we cannot, we'll simply apply
65
84
  # the ordering in pass2, which will happen after a potential filter().
66
85
 
67
- if rql.body.type == Term::TermType::TABLE && !without_index?
86
+ NoBrainer::RQL.is_table?(rql)
87
+ if NoBrainer::RQL.is_table?(rql) && !without_index?
68
88
  options = {}
69
- first_key = effective_order.first[0]
89
+ first_key = _effective_order.first[0]
70
90
  if (first_key.is_a?(Symbol) || first_key.is_a?(String)) && klass.has_index?(first_key)
71
91
  options[:index] = rql_rules.shift
72
92
  end
@@ -83,7 +103,10 @@ module NoBrainer::Criteria::OrderBy
83
103
 
84
104
  def compile_rql_pass2
85
105
  rql = super
86
- rql = rql.order_by(*@rql_rules_pass2) if @rql_rules_pass2
106
+ if @rql_rules_pass2
107
+ rql = rql.order_by(*@rql_rules_pass2)
108
+ @rql_rules_pass2 = nil
109
+ end
87
110
  rql
88
111
  end
89
112
 
@@ -2,10 +2,10 @@ module NoBrainer::Criteria::Update
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def update_all(*args, &block)
5
- run(to_rql.update(*args, &block))
5
+ run(without_ordering.to_rql.update(*args, &block))
6
6
  end
7
7
 
8
8
  def replace_all(*args, &block)
9
- run(to_rql.replace(*args, &block))
9
+ run(without_ordering.to_rql.replace(*args, &block))
10
10
  end
11
11
  end
@@ -92,7 +92,7 @@ module NoBrainer::Criteria::Where
92
92
  target_klass = association.target_klass
93
93
  opts = { :attr_name => key, :value => value, :type => target_klass}
94
94
  raise NoBrainer::Error::InvalidType.new(opts) unless value.is_a?(target_klass)
95
- value.id
95
+ value.pk_value
96
96
  else
97
97
  criteria.klass.cast_user_to_db_for(key, value)
98
98
  end
@@ -2,13 +2,18 @@ class NoBrainer::Document::Association::BelongsTo
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:foreign_key, :class_name, :index, :validates, :required]
5
+ VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :index, :validates, :required]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
  extend NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
9
9
  def foreign_key
10
10
  # TODO test :foreign_key
11
- options[:foreign_key].try(:to_sym) || :"#{target_name}_id"
11
+ options[:foreign_key].try(:to_sym) || :"#{target_name}_#{primary_key}"
12
+ end
13
+
14
+ def primary_key
15
+ # TODO test :primary_key
16
+ options[:primary_key].try(:to_sym) || target_klass.pk_name
12
17
  end
13
18
 
14
19
  def target_klass
@@ -32,7 +37,7 @@ class NoBrainer::Document::Association::BelongsTo
32
37
  add_callback_for(:after_validation)
33
38
  end
34
39
 
35
- eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ :id },
40
+ eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ primary_key },
36
41
  :unscoped => true
37
42
  end
38
43
 
@@ -49,13 +54,13 @@ class NoBrainer::Document::Association::BelongsTo
49
54
  return target if loaded?
50
55
 
51
56
  if fk = owner.read_attribute(foreign_key)
52
- preload(target_klass.find(fk))
57
+ preload(target_klass.unscoped.where(primary_key => fk).first)
53
58
  end
54
59
  end
55
60
 
56
61
  def write(target)
57
62
  assert_target_type(target)
58
- owner.write_attribute(foreign_key, target.try(:id))
63
+ owner.write_attribute(foreign_key, target.try(:pk_value))
59
64
  preload(target)
60
65
  end
61
66
 
@@ -49,7 +49,7 @@ module NoBrainer::Document::Association::Core
49
49
 
50
50
  included { attr_accessor :metadata, :owner }
51
51
 
52
- delegate :foreign_key, :target_name, :target_klass, :to => :metadata
52
+ delegate :primary_key, :foreign_key, :target_name, :target_klass, :to => :metadata
53
53
 
54
54
  def initialize(metadata, owner)
55
55
  @metadata, @owner = metadata, owner
@@ -2,13 +2,18 @@ class NoBrainer::Document::Association::HasMany
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:foreign_key, :class_name, :dependent]
5
+ VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
  extend NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
9
9
  def foreign_key
10
10
  # TODO test :foreign_key
11
- options[:foreign_key].try(:to_sym) || owner_klass.name.foreign_key.to_sym
11
+ options[:foreign_key].try(:to_sym) || :"#{owner_klass.name.underscore}_#{owner_klass.pk_name}"
12
+ end
13
+
14
+ def primary_key
15
+ # TODO test :primary_key
16
+ options[:primary_key].try(:to_sym) || target_klass.pk_name
12
17
  end
13
18
 
14
19
  def target_klass
@@ -25,6 +30,7 @@ class NoBrainer::Document::Association::HasMany
25
30
  target_klass.association_metadata.values.select do |assoc|
26
31
  assoc.is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) and
27
32
  assoc.foreign_key == self.foreign_key and
33
+ assoc.primary_key == self.primary_key and
28
34
  assoc.target_klass.root_class == owner_klass.root_class
29
35
  end
30
36
  end
@@ -34,11 +40,11 @@ class NoBrainer::Document::Association::HasMany
34
40
  add_callback_for(:before_destroy) if options[:dependent]
35
41
  end
36
42
 
37
- eager_load_with :owner_key => ->{ :id }, :target_key => ->{ foreign_key }
43
+ eager_load_with :owner_key => ->{ primary_key }, :target_key => ->{ foreign_key }
38
44
  end
39
45
 
40
46
  def target_criteria
41
- @target_criteria ||= target_klass.where(foreign_key => owner.id)
47
+ @target_criteria ||= target_klass.where(foreign_key => owner.pk_value)
42
48
  .after_find(set_inverse_proc)
43
49
  end
44
50
 
@@ -1,6 +1,9 @@
1
1
  module NoBrainer::Document::Attributes
2
- VALID_FIELD_OPTIONS = [:index, :default, :type, :cast_user_to_db, :cast_db_to_user, :validates, :required, :unique, :readonly]
3
- RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations] + NoBrainer::DecoratedSymbol::MODIFIERS.keys
2
+ VALID_FIELD_OPTIONS = [:index, :default, :type, :cast_user_to_db,
3
+ :cast_db_to_user, :validates, :required, :unique,
4
+ :readonly, :primary_key]
5
+ RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] \
6
+ + NoBrainer::DecoratedSymbol::MODIFIERS.keys
4
7
  extend ActiveSupport::Concern
5
8
 
6
9
  included do
@@ -15,8 +18,12 @@ module NoBrainer::Document::Attributes
15
18
  assign_attributes(attrs, options.reverse_merge(:pristine => true))
16
19
  end
17
20
 
21
+ def readable_attributes
22
+ @_attributes.keys & self.class.fields.keys.map(&:to_s)
23
+ end
24
+
18
25
  def attributes
19
- Hash[@_attributes.keys.map { |k| [k, read_attribute(k)] }].with_indifferent_access.freeze
26
+ Hash[readable_attributes.map { |k| [k, read_attribute(k)] }].with_indifferent_access.freeze
20
27
  end
21
28
 
22
29
  def read_attribute(name)
@@ -97,6 +104,23 @@ module NoBrainer::Document::Attributes
97
104
  _field(attr, self.fields[attr])
98
105
  end
99
106
 
107
+ def _remove_field(attr, options={})
108
+ inject_in_layer :attributes do
109
+ remove_method("#{attr}=")
110
+ remove_method("#{attr}")
111
+ end
112
+ end
113
+
114
+ def remove_field(attr, options={})
115
+ attr = attr.to_sym
116
+
117
+ _remove_field(attr, options)
118
+
119
+ ([self] + descendants).each do |klass|
120
+ klass.fields.delete(attr)
121
+ end
122
+ end
123
+
100
124
  def has_field?(attr)
101
125
  !!fields[attr.to_sym]
102
126
  end
@@ -1,19 +1,11 @@
1
1
  module NoBrainer::Document::Callbacks
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def self.terminator
5
- if Gem.loaded_specs['activesupport'].version.release >= Gem::Version.new('4.1')
6
- proc { false }
7
- else
8
- 'false'
9
- end
10
- end
11
-
12
4
  included do
13
5
  extend ActiveModel::Callbacks
14
6
 
15
- define_model_callbacks :initialize, :create, :update, :save, :destroy, :terminator => NoBrainer::Document::Callbacks.terminator
16
- define_model_callbacks :find, :only => [:after], :terminator => NoBrainer::Document::Callbacks.terminator
7
+ define_model_callbacks :initialize, :create, :update, :save, :destroy, :terminator => proc { false }
8
+ define_model_callbacks :find, :only => [:after], :terminator => proc { false }
17
9
  end
18
10
 
19
11
  def initialize(*args, &block)
@@ -5,16 +5,18 @@ module NoBrainer::Document::Core
5
5
  attr_accessor :_all
6
6
 
7
7
  def all
8
- Rails.application.eager_load! if defined?(Rails)
8
+ Rails.application.eager_load! if defined?(Rails.application.eager_load!)
9
9
  @_all
10
10
  end
11
11
  end
12
12
  self._all = []
13
13
 
14
- # TODO This assume the primary key is id.
15
- # RethinkDB can have a custom primary key. careful.
16
14
  include ActiveModel::Conversion
17
15
 
16
+ def to_key
17
+ [pk_value]
18
+ end
19
+
18
20
  included do
19
21
  # TODO test these includes
20
22
  extend ActiveModel::Naming
@@ -2,7 +2,7 @@ module NoBrainer::Document::Criteria
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def selector
5
- self.class.selector_for(id)
5
+ self.class.selector_for(pk_value)
6
6
  end
7
7
 
8
8
  included { cattr_accessor :default_scope_proc, :instance_accessor => false }
@@ -40,20 +40,20 @@ module NoBrainer::Document::Criteria
40
40
  self.default_scope_proc = criteria.is_a?(Proc) ? criteria : proc { criteria }
41
41
  end
42
42
 
43
- def selector_for(id)
44
- # TODO Pass primary key if not default
45
- unscoped.where(:id => id)
43
+ def selector_for(pk)
44
+ rql_table.get(pk)
46
45
  end
47
46
 
48
47
  # XXX this doesn't have the same semantics as
49
48
  # other ORMs. the equivalent is find!.
50
- def find(id)
51
- selector_for(id).first
49
+ def find(pk)
50
+ attrs = NoBrainer.run { selector_for(pk) }
51
+ new_from_db(attrs).tap { |doc| doc.run_callbacks(:find) } if attrs
52
52
  end
53
53
 
54
- def find!(id)
55
- find(id).tap do |doc|
56
- raise NoBrainer::Error::DocumentNotFound, "#{self.class} id #{id} not found" unless doc
54
+ def find!(pk)
55
+ find(pk).tap do |doc|
56
+ raise NoBrainer::Error::DocumentNotFound, "#{self} #{pk_name}: #{pk} not found" unless doc
57
57
  end
58
58
  end
59
59
  end
@@ -79,5 +79,16 @@ module NoBrainer::Document::Dirty
79
79
  end
80
80
  end
81
81
  end
82
+
83
+ def _remove_field(attr, options={})
84
+ super
85
+ inject_in_layer :dirty_tracking do
86
+ remove_method("#{attr}_change")
87
+ remove_method("#{attr}_changed?")
88
+ remove_method("#{attr}_was")
89
+ remove_method("#{attr}=")
90
+ remove_method("#{attr}")
91
+ end
92
+ end
82
93
  end
83
94
  end
@@ -17,4 +17,8 @@ module NoBrainer::Document::DynamicAttributes
17
17
  @_attributes[name] = value
18
18
  end
19
19
  end
20
+
21
+ def readable_attributes
22
+ @_attributes.keys
23
+ end
20
24
  end
@@ -5,17 +5,23 @@ require 'digest/md5'
5
5
  module NoBrainer::Document::Id
6
6
  extend ActiveSupport::Concern
7
7
 
8
- included do
9
- self.field :id, :type => String, :default => ->{ NoBrainer::Document::Id.generate }, :readonly => true
8
+ DEFAULT_PK_NAME = :id
9
+
10
+ def pk_value
11
+ __send__(self.class.pk_name)
12
+ end
13
+
14
+ def pk_value=(value)
15
+ __send__("#{self.class.pk_name}=", value)
10
16
  end
11
17
 
12
18
  def ==(other)
13
19
  return super unless self.class == other.class
14
- !id.nil? && id == other.id
20
+ !pk_value.nil? && pk_value == other.pk_value
15
21
  end
16
22
  alias_method :eql?, :==
17
23
 
18
- delegate :hash, :to => :id
24
+ delegate :hash, :to => :pk_value
19
25
 
20
26
  # The following code is inspired by the mongo-ruby-driver
21
27
 
@@ -46,4 +52,43 @@ module NoBrainer::Document::Id
46
52
 
47
53
  oid.unpack("C12").map {|e| v=e.to_s(16); v.size == 1 ? "0#{v}" : v }.join
48
54
  end
55
+
56
+ module ClassMethods
57
+ def define_default_pk
58
+ class_variable_set(:@@pk_name, nil)
59
+ field NoBrainer::Document::Id::DEFAULT_PK_NAME, :primary_key => :default,
60
+ :type => String, :default => ->{ NoBrainer::Document::Id.generate }
61
+ end
62
+
63
+ def define_pk(attr)
64
+ if fields[pk_name].try(:[], :primary_key) == :default
65
+ remove_field(pk_name, :set_default_pk => false)
66
+ end
67
+ class_variable_set(:@@pk_name, attr)
68
+ end
69
+
70
+ def pk_name
71
+ class_variable_get(:@@pk_name)
72
+ end
73
+
74
+ def _field(attr, options={})
75
+ super
76
+ define_pk(attr) if options[:primary_key]
77
+ end
78
+
79
+ def field(attr, options={})
80
+ if options[:primary_key]
81
+ options = options.merge(:readonly => true) if options[:readonly].nil?
82
+ options = options.merge(:index => true)
83
+ end
84
+ super
85
+ end
86
+
87
+ def _remove_field(attr, options={})
88
+ super
89
+ if fields[attr][:primary_key] && options[:set_default_pk] != false
90
+ define_default_pk
91
+ end
92
+ end
93
+ end
49
94
  end
@@ -5,7 +5,6 @@ module NoBrainer::Document::Index
5
5
  included do
6
6
  cattr_accessor :indexes, :instance_accessor => false
7
7
  self.indexes = {}
8
- self.index :id
9
8
  end
10
9
 
11
10
  module ClassMethods
@@ -57,6 +56,11 @@ module NoBrainer::Document::Index
57
56
  end
58
57
  end
59
58
 
59
+ def _remove_field(attr, options={})
60
+ super
61
+ remove_index(attr) if fields[attr][:index]
62
+ end
63
+
60
64
  def perform_create_index(index_name, options={})
61
65
  index_name = index_name.to_sym
62
66
  index_args = self.indexes[index_name]
@@ -79,7 +83,7 @@ module NoBrainer::Document::Index
79
83
 
80
84
  def perform_update_indexes(options={})
81
85
  current_indexes = NoBrainer.run(self.rql_table.index_list).map(&:to_sym)
82
- wanted_indexes = self.indexes.keys - [:id] # XXX Primary key?
86
+ wanted_indexes = self.indexes.keys - [self.pk_name]
83
87
 
84
88
  (current_indexes - wanted_indexes).each do |index_name|
85
89
  perform_drop_index(index_name, options)
@@ -19,7 +19,8 @@ module NoBrainer::Document::Persistance
19
19
  end
20
20
 
21
21
  def reload(options={})
22
- attrs = selector.raw.first!
22
+ attrs = NoBrainer.run { self.class.selector_for(pk_value) }
23
+ raise NoBrainer::Error::DocumentNotFound, "#{self.class} #{self.class.pk_name}: #{pk_value} not found" unless attrs
23
24
  instance_variables.each { |ivar| remove_instance_variable(ivar) } unless options[:keep_ivars]
24
25
  initialize(attrs, :pristine => true, :from_db => true)
25
26
  self
@@ -28,13 +29,13 @@ module NoBrainer::Document::Persistance
28
29
  def _create(options={})
29
30
  return false if options[:validate] && !valid?
30
31
  keys = self.class.insert_all(@_attributes)
31
- self.id ||= keys.first
32
+ self.pk_value ||= keys.first
32
33
  @new_record = false
33
34
  true
34
35
  end
35
36
 
36
37
  def _update(attrs)
37
- selector.update_all(attrs)
38
+ NoBrainer.run { selector.update(attrs) }
38
39
  end
39
40
 
40
41
  def _update_only_changed_attrs(options={})
@@ -73,7 +74,7 @@ module NoBrainer::Document::Persistance
73
74
 
74
75
  def delete
75
76
  unless @destroyed
76
- selector.delete_all
77
+ NoBrainer.run { selector.delete }
77
78
  @destroyed = true
78
79
  end
79
80
  @_attributes.freeze
@@ -15,5 +15,12 @@ module NoBrainer::Document::Readonly
15
15
  end
16
16
  end
17
17
  end
18
+
19
+ def _remove_field(attr, options={})
20
+ super
21
+ inject_in_layer :readonly do
22
+ remove_method("#{attr}=") if method_defined?("#{attr}=")
23
+ end
24
+ end
18
25
  end
19
26
  end
@@ -5,4 +5,8 @@ module NoBrainer::Document::Serialization
5
5
  include ActiveModel::Serializers::JSON
6
6
 
7
7
  included { self.include_root_in_json = false }
8
+
9
+ def read_attribute_for_serialization(*a, &b)
10
+ read_attribute(*a, &b)
11
+ end
8
12
  end
@@ -171,5 +171,13 @@ module NoBrainer::Document::Types
171
171
  end
172
172
  super
173
173
  end
174
+
175
+ def _remove_field(attr, options={})
176
+ super
177
+ inject_in_layer :types do
178
+ remove_method("#{attr}=")
179
+ remove_method("#{attr}?") if method_defined?("#{attr}?")
180
+ end
181
+ end
174
182
  end
175
183
  end
@@ -64,7 +64,9 @@ module NoBrainer::Document::Uniqueness
64
64
  class UniquenessValidator < ActiveModel::EachValidator
65
65
  attr_accessor :scope
66
66
 
67
- def setup(klass)
67
+ def initialize(options={})
68
+ super
69
+ klass = options[:class]
68
70
  self.scope = [*options[:scope]]
69
71
  ([klass] + klass.descendants).each do |_klass|
70
72
  _klass.unique_validators << self
@@ -91,7 +93,7 @@ module NoBrainer::Document::Uniqueness
91
93
  end
92
94
 
93
95
  def exclude_doc(criteria, doc)
94
- criteria.where(:id.ne => doc.id)
96
+ criteria.where(doc.class.pk_name.ne => doc.pk_value)
95
97
  end
96
98
  end
97
99
  end
@@ -6,7 +6,7 @@ module NoBrainer::Document::Validation
6
6
  included do
7
7
  # We don't want before_validation returning false to halt the chain.
8
8
  define_callbacks :validation, :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name],
9
- :terminator => NoBrainer::Document::Callbacks.terminator
9
+ :terminator => proc { false }
10
10
  end
11
11
 
12
12
  def valid?(context=nil)
@@ -10,5 +10,7 @@ module NoBrainer::Document
10
10
 
11
11
  autoload :DynamicAttributes, :Timestamps
12
12
 
13
+ included { define_default_pk }
14
+
13
15
  singleton_class.delegate :all, :to => Core
14
16
  end
@@ -1,13 +1,13 @@
1
1
  module NoBrainer::Error
2
- class Connection < RuntimeError; end
3
- class DocumentNotFound < RuntimeError; end
4
- class DocumentNotSaved < RuntimeError; end
5
- class ChildrenExist < RuntimeError; end
6
- class CannotUseIndex < RuntimeError; end
7
- class MissingIndex < RuntimeError; end
8
- class InvalidType < RuntimeError; end
2
+ class Connection < RuntimeError; end
3
+ class DocumentNotFound < RuntimeError; end
4
+ class DocumentNotPersisted < RuntimeError; end
5
+ class ChildrenExist < RuntimeError; end
6
+ class CannotUseIndex < RuntimeError; end
7
+ class MissingIndex < RuntimeError; end
8
+ class InvalidType < RuntimeError; end
9
9
  class AssociationNotPersisted < RuntimeError; end
10
- class ReadonlyField < RuntimeError; end
10
+ class ReadonlyField < RuntimeError; end
11
11
 
12
12
  class DocumentInvalid < RuntimeError
13
13
  attr_accessor :instance
@@ -13,23 +13,29 @@ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
13
13
  return unless NoBrainer.logger.debug?
14
14
 
15
15
  duration = Time.now - start_time
16
- msg = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
17
16
 
18
- msg = "(#{env[:db_name]}) #{msg}" if env[:db_name]
19
- msg = "[#{(duration * 1000.0).round(1)}ms] #{msg}"
17
+ msg_duration = (duration * 1000.0).round(1).to_s
18
+ msg_duration = " " * [0, 5 - msg_duration.size].max + msg_duration
19
+ msg_duration = "[#{msg_duration}ms] "
20
+
21
+ msg_db = "[#{env[:db_name]}] " if env[:db_name]
22
+ msg_query = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
23
+ msg_exception = " #{exception.class} #{exception.message.split("\n").first}" if exception
24
+ msg_last = nil
20
25
 
21
26
  if NoBrainer::Config.colorize_logger
22
- if exception
23
- msg = "#{msg} \e[0;31m#{exception.class} #{exception.message.split("\n").first}\e[0m"
24
- else
25
- case NoBrainer::Util.rql_type(env[:query])
26
- when :write then msg = "\e[1;31m#{msg}\e[0m" # red
27
- when :read then msg = "\e[1;32m#{msg}\e[0m" # green
28
- when :management then msg = "\e[1;33m#{msg}\e[0m" # yellow
29
- end
30
- end
27
+ query_color = case NoBrainer::RQL.type_of(env[:query])
28
+ when :write then "\e[1;31m" # red
29
+ when :read then "\e[1;32m" # green
30
+ when :management then "\e[1;33m" # yellow
31
+ end
32
+ msg_duration = [query_color, msg_duration].join
33
+ msg_db = ["\e[0;34m", msg_db, query_color].join if msg_db
34
+ msg_exception = ["\e[0;31m", msg_exception].join if msg_exception
35
+ msg_last = "\e[0m"
31
36
  end
32
37
 
38
+ msg = [msg_duration, msg_db, msg_query, msg_exception, msg_last].join
33
39
  NoBrainer.logger.debug(msg)
34
40
  end
35
41
  end
@@ -2,9 +2,21 @@ class NoBrainer::QueryRunner::MissingIndex < NoBrainer::QueryRunner::Middleware
2
2
  def call(env)
3
3
  @runner.call(env)
4
4
  rescue RethinkDB::RqlRuntimeError => e
5
- if e.message =~ /^Index `(.+)` was not found\.$/
6
- raise NoBrainer::Error::MissingIndex.new("Please run \"rake db:update_indexes\" to create the index `#{$1}`\n" +
7
- "--> Read http://nobrainer.io/docs/indexes for more information.")
5
+ if e.message =~ /^Index `(.+)` was not found on table `(.+)\.(.+)`\.$/
6
+ index_name = $1
7
+ database_name = $2
8
+ table_name = $3
9
+
10
+ klass = NoBrainer::Document.all.select { |m| m.table_name == table_name }.first
11
+ if klass && klass.pk_name.to_s == index_name
12
+ err_msg = "Please run update the primary key `#{index_name}` in the table `#{database_name}.#{table_name}`."
13
+ else
14
+ err_msg = "Please run \"rake db:update_indexes\" to create the index `#{index_name}`"
15
+ err_msg += " in the table `#{database_name}.#{table_name}`."
16
+ err_msg += "\n--> Read http://nobrainer.io/docs/indexes for more information."
17
+ end
18
+
19
+ raise NoBrainer::Error::MissingIndex.new(err_msg)
8
20
  end
9
21
  raise
10
22
  end
@@ -4,20 +4,20 @@ class NoBrainer::QueryRunner::Reconnect < NoBrainer::QueryRunner::Middleware
4
4
  rescue StandardError => e
5
5
  # TODO test that thing
6
6
  if is_connection_error_exception?(e)
7
- retry if reconnect
7
+ retry if reconnect(e)
8
8
  end
9
9
  raise
10
10
  end
11
11
 
12
12
  private
13
13
 
14
- def reconnect
14
+ def reconnect(e)
15
15
  # FIXME thread safety? perhaps we need to use a connection pool
16
16
  # XXX Possibly dangerous, as we could reexecute a non idempotent operation
17
17
  # Check the semantics of the db
18
18
  NoBrainer::Config.max_reconnection_tries.times do
19
19
  begin
20
- NoBrainer.logger.try(:warn, "Lost connection to #{NoBrainer::Config.rethinkdb_url}, retrying...")
20
+ warn_reconnect(e)
21
21
  sleep 1
22
22
  NoBrainer.connection.reconnect(:noreply_wait => false)
23
23
  return true
@@ -35,10 +35,21 @@ class NoBrainer::QueryRunner::Reconnect < NoBrainer::QueryRunner::Middleware
35
35
  Errno::ECONNRESET, Errno::ETIMEDOUT, IOError
36
36
  true
37
37
  when RethinkDB::RqlRuntimeError
38
- e.message =~ /cannot perform (read|write): No master available/ ||
38
+ e.message =~ /No master available/ ||
39
+ e.message =~ /Master .* not available/ ||
39
40
  e.message =~ /Error: Connection Closed/
40
41
  else
41
42
  false
42
43
  end
43
44
  end
45
+
46
+ def warn_reconnect(e)
47
+ if e.is_a?(RethinkDB::RqlRuntimeError)
48
+ e_msg = e.message.split("\n").first
49
+ msg = "Server #{NoBrainer::Config.rethinkdb_url} not ready - #{e_msg}, retrying..."
50
+ else
51
+ msg = "Connection issue with #{NoBrainer::Config.rethinkdb_url} - #{e}, retrying..."
52
+ end
53
+ NoBrainer.logger.try(:warn, msg)
54
+ end
44
55
  end
@@ -3,8 +3,8 @@ class NoBrainer::QueryRunner::TableOnDemand < NoBrainer::QueryRunner::Middleware
3
3
  @runner.call(env)
4
4
  rescue RuntimeError => e
5
5
  if NoBrainer::Config.auto_create_tables &&
6
- e.message =~ /^Table `(.+)` does not exist\.$/
7
- auto_create_table(env, $1)
6
+ e.message =~ /^Table `(.+)\.(.+)` does not exist\.$/
7
+ auto_create_table(env, $1, $2)
8
8
  retry
9
9
  end
10
10
  raise
@@ -12,33 +12,22 @@ class NoBrainer::QueryRunner::TableOnDemand < NoBrainer::QueryRunner::Middleware
12
12
 
13
13
  private
14
14
 
15
- def auto_create_table(env, table_name)
16
- if env[:auto_create_table] == table_name
17
- raise "Auto table creation is not working with #{table_name}"
15
+ def auto_create_table(env, database_name, table_name)
16
+ klass = NoBrainer::Document.all.select { |m| m.table_name == table_name }.first
17
+ if klass.nil?
18
+ raise "Auto table creation is not working for `#{database_name}.#{table_name}` -- Can't find the corresponding model."
18
19
  end
19
- env[:auto_create_table] = table_name
20
20
 
21
- # FIXME This stinks.
22
- database_names = find_db_names(env[:query])
23
- case database_names.size
24
- when 0 then NoBrainer.table_create(table_name)
25
- when 1 then NoBrainer.with_database(database_names.first) { NoBrainer.table_create(table_name) }
26
- else raise "Ambiguous database name for creation on demand: #{database_names}"
21
+ if env[:auto_create_table] == [database_name, table_name]
22
+ raise "Auto table creation is not working for `#{database_name}.#{table_name}`"
23
+ end
24
+ env[:auto_create_table] = [database_name, table_name]
25
+
26
+ NoBrainer.with_database(database_name) do
27
+ NoBrainer.table_create(table_name, :primary_key => klass.pk_name)
27
28
  end
28
29
  rescue RuntimeError => e
29
30
  # We might have raced with another table create
30
- raise unless e.message =~ /Table `#{table_name}` already exists/
31
- end
32
-
33
- def find_db_names(terms)
34
- terms = terms.body.args if terms.is_a?(RethinkDB::RQL)
35
- terms.map do |term|
36
- next unless term.is_a?(Term)
37
- if term.type == Term::TermType::DB
38
- term.args.first.datum.r_str
39
- else
40
- find_db_names(term.args)
41
- end
42
- end.flatten.uniq
31
+ raise unless e.message =~ /Table `#{database_name}\.#{table_name}` already exists/
43
32
  end
44
33
  end
@@ -1,13 +1,10 @@
1
1
  class NoBrainer::QueryRunner::WriteError < NoBrainer::QueryRunner::Middleware
2
2
  def call(env)
3
- write_query = NoBrainer::Util.is_write_query?(env[:query])
3
+ write_query = NoBrainer::RQL.is_write_query?(env[:query])
4
4
  @runner.call(env).tap do |result|
5
- # TODO Fix rethinkdb driver: Their classes Term, Query, Response are
6
- # not scoped to the RethinkDB module! (that would prevent a user from
7
- # creating a Response model for example).
8
-
9
- if write_query && (result['errors'].to_i != 0 || result['skipped'].to_i != 0)
10
- raise_write_error(env, result['first_error'])
5
+ if write_query && (result['errors'].to_i != 0)
6
+ error_msg = result['first_error']
7
+ raise_write_error(env, error_msg)
11
8
  end
12
9
  end
13
10
  rescue RethinkDB::RqlRuntimeError => e
@@ -23,6 +20,6 @@ class NoBrainer::QueryRunner::WriteError < NoBrainer::QueryRunner::Middleware
23
20
  def raise_write_error(env, error_msg)
24
21
  error_msg ||= "Unknown error"
25
22
  error_msg += "\nQuery was: #{env[:query].inspect[0..1000]}"
26
- raise NoBrainer::Error::DocumentNotSaved, error_msg
23
+ raise NoBrainer::Error::DocumentNotPersisted, error_msg
27
24
  end
28
25
  end
@@ -0,0 +1,25 @@
1
+ module NoBrainer::RQL
2
+ include RethinkDB::Term::TermType
3
+ extend self
4
+
5
+ def is_write_query?(rql_query)
6
+ type_of(rql_query) == :write
7
+ end
8
+
9
+ def type_of(rql_query)
10
+ case rql_query.body.first
11
+ when UPDATE, DELETE, REPLACE, INSERT
12
+ :write
13
+ when DB_CREATE,DB_DROP, DB_LIST, TABLE_CREATE, TABLE_DROP, TABLE_LIST, SYNC,
14
+ INDEX_CREATE, INDEX_DROP, INDEX_LIST, INDEX_STATUS, INDEX_WAIT
15
+ :management
16
+ else
17
+ # XXX Not sure if that's correct, but we'll be happy for logging colors.
18
+ :read
19
+ end
20
+ end
21
+
22
+ def is_table?(rql)
23
+ rql.body.first == TABLE
24
+ end
25
+ end
data/lib/nobrainer.rb CHANGED
@@ -7,19 +7,18 @@ module NoBrainer
7
7
  require 'no_brainer/autoload'
8
8
  extend NoBrainer::Autoload
9
9
 
10
- # We eager load things that could be loaded for the first time during the web request
10
+ # We eager load things that could be loaded when handling the first web request.
11
+ # Code that is loaded through the DSL of NoBrainer should not be eager loaded.
11
12
  autoload :Document, :IndexManager, :Loader, :Fork, :DecoratedSymbol
12
- eager_autoload :Config, :Connection, :Error, :QueryRunner, :Criteria, :Util
13
+ eager_autoload :Config, :Connection, :Error, :QueryRunner, :Criteria, :RQL
13
14
 
14
15
  class << self
15
- # Note: we always access the connection explicitly, so that in the future,
16
- # we can refactor to return a connection depending on the context.
17
- # Note that a connection is tied to a database in NoBrainer.
16
+ # A connection is tied to a database.
18
17
  def connection
19
18
  @connection ||= begin
20
19
  url = NoBrainer::Config.rethinkdb_url
21
20
  raise "Please specify a database connection to RethinkDB" unless url
22
- Connection.new(url).tap { |c| c.connect }
21
+ Connection.new(url)
23
22
  end
24
23
  end
25
24
 
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nobrainer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Viennot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-31 00:00:00.000000000 Z
11
+ date: 2014-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rethinkdb
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.12.0.1
19
+ version: 1.13.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 1.12.0.1
26
+ version: 1.13.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 4.0.0
33
+ version: 4.1.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 4.0.0
40
+ version: 4.1.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: activemodel
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 4.0.0
47
+ version: 4.1.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 4.0.0
54
+ version: 4.1.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: middleware
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -137,7 +137,7 @@ files:
137
137
  - lib/no_brainer/query_runner/write_error.rb
138
138
  - lib/no_brainer/railtie.rb
139
139
  - lib/no_brainer/railtie/database.rake
140
- - lib/no_brainer/util.rb
140
+ - lib/no_brainer/rql.rb
141
141
  - lib/nobrainer.rb
142
142
  - lib/rails/generators/nobrainer.rb
143
143
  - lib/rails/generators/nobrainer/model/model_generator.rb
@@ -1,23 +0,0 @@
1
- module NoBrainer::Util
2
- def self.is_write_query?(rql_query)
3
- rql_type(rql_query) == :write
4
- end
5
-
6
- def self.rql_type(rql_query)
7
- case rql_query.body.type
8
- when Term::TermType::UPDATE, Term::TermType::DELETE,
9
- Term::TermType::REPLACE, Term::TermType::INSERT
10
- :write
11
- when Term::TermType::DB_CREATE, Term::TermType::DB_DROP,
12
- Term::TermType::DB_LIST, Term::TermType::TABLE_CREATE,
13
- Term::TermType::TABLE_DROP, Term::TermType::TABLE_LIST,
14
- Term::TermType::SYNC, Term::TermType::INDEX_CREATE,
15
- Term::TermType::INDEX_DROP, Term::TermType::INDEX_LIST,
16
- Term::TermType::INDEX_STATUS, Term::TermType::INDEX_WAIT
17
- :management
18
- else
19
- # XXX Not sure if that's correct, but we'll be happy for logging colors.
20
- :read
21
- end
22
- end
23
- end