nobrainer 0.13.1 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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