nobrainer 0.20.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +0 -5
  3. data/lib/no_brainer/config.rb +14 -9
  4. data/lib/no_brainer/connection.rb +1 -3
  5. data/lib/no_brainer/criteria.rb +1 -1
  6. data/lib/no_brainer/criteria/aggregate.rb +2 -2
  7. data/lib/no_brainer/criteria/cache.rb +8 -3
  8. data/lib/no_brainer/criteria/core.rb +1 -5
  9. data/lib/no_brainer/criteria/delete.rb +1 -1
  10. data/lib/no_brainer/criteria/eager_load.rb +51 -0
  11. data/lib/no_brainer/criteria/order_by.rb +1 -1
  12. data/lib/no_brainer/criteria/scope.rb +3 -10
  13. data/lib/no_brainer/criteria/update.rb +8 -6
  14. data/lib/no_brainer/criteria/where.rb +50 -13
  15. data/lib/no_brainer/document.rb +2 -2
  16. data/lib/no_brainer/document/aliases.rb +0 -8
  17. data/lib/no_brainer/document/association/belongs_to.rb +6 -2
  18. data/lib/no_brainer/document/association/core.rb +5 -4
  19. data/lib/no_brainer/document/association/eager_loader.rb +7 -8
  20. data/lib/no_brainer/document/association/has_many.rb +22 -8
  21. data/lib/no_brainer/document/association/has_many_through.rb +12 -3
  22. data/lib/no_brainer/document/atomic_ops.rb +63 -61
  23. data/lib/no_brainer/document/attributes.rb +11 -3
  24. data/lib/no_brainer/document/core.rb +5 -2
  25. data/lib/no_brainer/document/criteria.rb +14 -5
  26. data/lib/no_brainer/document/dirty.rb +11 -16
  27. data/lib/no_brainer/document/index.rb +0 -6
  28. data/lib/no_brainer/document/index/meta_store.rb +1 -1
  29. data/lib/no_brainer/document/persistance.rb +12 -2
  30. data/lib/no_brainer/document/types.rb +13 -12
  31. data/lib/no_brainer/document/types/binary.rb +0 -4
  32. data/lib/no_brainer/document/types/boolean.rb +0 -1
  33. data/lib/no_brainer/document/types/geo.rb +1 -0
  34. data/lib/no_brainer/document/types/string.rb +3 -0
  35. data/lib/no_brainer/document/types/text.rb +18 -0
  36. data/lib/no_brainer/document/validation.rb +31 -6
  37. data/lib/no_brainer/document/validation/not_null.rb +15 -0
  38. data/lib/no_brainer/document/{uniqueness.rb → validation/uniqueness.rb} +11 -10
  39. data/lib/no_brainer/error.rb +20 -23
  40. data/lib/no_brainer/locale/en.yml +1 -0
  41. data/lib/no_brainer/lock.rb +114 -0
  42. data/lib/no_brainer/query_runner/database_on_demand.rb +0 -1
  43. data/lib/no_brainer/query_runner/missing_index.rb +1 -1
  44. data/lib/no_brainer/query_runner/run_options.rb +0 -3
  45. data/lib/no_brainer/query_runner/table_on_demand.rb +2 -3
  46. data/lib/no_brainer/rql.rb +0 -4
  47. data/lib/nobrainer.rb +1 -1
  48. metadata +8 -4
  49. data/lib/no_brainer/criteria/preload.rb +0 -44
@@ -2,7 +2,7 @@ module NoBrainer::Document::Core
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  singleton_class.class_eval do
5
- attr_accessor :_all
5
+ attr_accessor :_all, :_all_nobrainer
6
6
 
7
7
  def all
8
8
  Rails.application.eager_load! if defined?(Rails.application.eager_load!)
@@ -10,10 +10,12 @@ module NoBrainer::Document::Core
10
10
  end
11
11
  end
12
12
  self._all = []
13
+ self._all_nobrainer = []
13
14
 
14
15
  include ActiveModel::Conversion
15
16
 
16
17
  def to_key
18
+ # ActiveModel::Conversion stuff
17
19
  [pk_value]
18
20
  end
19
21
 
@@ -22,6 +24,7 @@ module NoBrainer::Document::Core
22
24
  extend ActiveModel::Naming
23
25
  extend ActiveModel::Translation
24
26
 
25
- NoBrainer::Document::Core._all << self unless self.name =~ /^NoBrainer::/
27
+ list_name = self.name =~ /^NoBrainer::/ ? :_all_nobrainer : :_all
28
+ NoBrainer::Document::Core.__send__(list_name) << self
26
29
  end
27
30
  end
@@ -6,8 +6,9 @@ module NoBrainer::Document::Criteria
6
6
  end
7
7
 
8
8
  included do
9
- cattr_accessor :default_scope_proc, :instance_accessor => false
10
9
  cattr_accessor :perf_warnings_disabled, :instance_accessor => false
10
+ singleton_class.send(:attr_accessor, :default_scopes)
11
+ self.default_scopes = []
11
12
  end
12
13
 
13
14
  module ClassMethods
@@ -21,7 +22,7 @@ module NoBrainer::Document::Criteria
21
22
  :with_cache, :without_cache, # Cache
22
23
  :count, :empty?, :any?, # Count
23
24
  :delete_all, :destroy_all, # Delete
24
- :includes, :preload, # Preload
25
+ :preload, :eager_load, # EagerLoad
25
26
  :each, :to_a, # Enumerable
26
27
  :first, :last, :first!, :last!, :sample, # First
27
28
  :min, :max, :sum, :avg, # Aggregate
@@ -42,9 +43,17 @@ module NoBrainer::Document::Criteria
42
43
  end
43
44
 
44
45
  def default_scope(criteria=nil, &block)
45
- criteria ||= block
46
- raise "store_in() must be called on the parent class" unless is_root_class?
47
- self.default_scope_proc = criteria.is_a?(Proc) ? criteria : proc { criteria }
46
+ criteria_proc = block || (criteria.is_a?(Proc) ? criteria : proc { criteria })
47
+ raise "default_scope only accepts a criteria or a proc that returns criteria" unless criteria_proc.is_a?(Proc)
48
+
49
+ ([self] + self.descendants).each do |model|
50
+ model.default_scopes << criteria_proc
51
+ end
52
+ end
53
+
54
+ def inherited(subclass)
55
+ subclass.default_scopes = self.default_scopes.dup
56
+ super
48
57
  end
49
58
 
50
59
  def selector_for(pk)
@@ -31,14 +31,10 @@ module NoBrainer::Document::Dirty
31
31
  changes.keys
32
32
  end
33
33
 
34
- def read_attribute_for_change(attr)
35
- read_attribute(attr)
36
- end
37
-
38
34
  def changes
39
35
  result = {}.with_indifferent_access
40
36
  @_old_attributes.each do |attr, old_value|
41
- current_value = read_attribute_for_change(attr)
37
+ current_value = read_attribute(attr)
42
38
  if current_value != old_value || !@_old_attributes_keys.include?(attr)
43
39
  result[attr] = [old_value, current_value]
44
40
  end
@@ -46,16 +42,15 @@ module NoBrainer::Document::Dirty
46
42
  result
47
43
  end
48
44
 
49
- def attribute_may_change(*args)
50
- attr = args.first
51
- current_value = begin
52
- case args.size
53
- when 1 then assert_access_field(attr); read_attribute_for_change(attr)
54
- when 2 then args.last
55
- else raise
45
+ class None; end
46
+ def attribute_may_change(attr, current_value = None)
47
+ if current_value == None
48
+ current_value = begin
49
+ assert_access_field(attr)
50
+ read_attribute(attr)
51
+ rescue NoBrainer::Error::MissingAttribute => e
52
+ e
56
53
  end
57
- rescue NoBrainer::Error::MissingAttribute => e
58
- e
59
54
  end
60
55
 
61
56
  unless @_old_attributes.has_key?(attr)
@@ -84,7 +79,7 @@ module NoBrainer::Document::Dirty
84
79
  inject_in_layer :dirty_tracking do
85
80
  define_method("#{attr}_change") do
86
81
  if @_old_attributes.has_key?(attr)
87
- result = [@_old_attributes[attr], read_attribute_for_change(attr)]
82
+ result = [@_old_attributes[attr], read_attribute(attr)]
88
83
  result if result.first != result.last || !@_old_attributes_keys.include?(attr)
89
84
  end
90
85
  end
@@ -94,7 +89,7 @@ module NoBrainer::Document::Dirty
94
89
  end
95
90
 
96
91
  define_method("#{attr}_was") do
97
- @_old_attributes.has_key?(attr) ? @_old_attributes[attr] : read_attribute_for_change(attr)
92
+ @_old_attributes.has_key?(attr) ? @_old_attributes[attr] : read_attribute(attr)
98
93
  end
99
94
  end
100
95
  end
@@ -14,12 +14,6 @@ module NoBrainer::Document::Index
14
14
  def index(name, *args)
15
15
  name = name.to_sym
16
16
  options = args.extract_options!
17
-
18
- if options[:as]
19
- STDERR.puts "[NoBrainer] `:as' is deprecated and will be removed. Please use `:store_as' instead (from the #{self} model)"
20
- options[:store_as] = options.delete(:as)
21
- end
22
-
23
17
  options.assert_valid_keys(*VALID_INDEX_OPTIONS)
24
18
 
25
19
  raise "Too many arguments: #{args}" if args.size > 1
@@ -24,7 +24,7 @@ class NoBrainer::Document::Index::MetaStore
24
24
  def self.on(db_name, &block)
25
25
  old_db_name = Thread.current[:nobrainer_meta_store_db]
26
26
  Thread.current[:nobrainer_meta_store_db] = db_name
27
- NoBrainer.with(:auto_create_tables => true) { block.call }
27
+ block.call
28
28
  ensure
29
29
  Thread.current[:nobrainer_meta_store_db] = old_db_name
30
30
  end
@@ -113,13 +113,11 @@ module NoBrainer::Document::Persistance
113
113
  assign_attributes(attrs, options)
114
114
  save?(options)
115
115
  end
116
- alias_method :update_attributes?, :update?
117
116
 
118
117
  def update(*args)
119
118
  update?(*args) or raise NoBrainer::Error::DocumentInvalid, self
120
119
  nil
121
120
  end
122
- alias_method :update_attributes, :update
123
121
 
124
122
  def update!(*args)
125
123
  update(*args)
@@ -127,6 +125,18 @@ module NoBrainer::Document::Persistance
127
125
  end
128
126
  alias_method :update_attributes!, :update!
129
127
 
128
+ def update_attributes?(*args)
129
+ update?(*args).tap { STDERR.puts "[NoBrainer] update_attribute?() is deprecated. Please use update?() instead" }
130
+ end
131
+
132
+ def update_attributes(*args)
133
+ update(*args).tap { STDERR.puts "[NoBrainer] update_attribute() is deprecated. Please use update() instead" }
134
+ end
135
+
136
+ def update_attributes!(*args)
137
+ update!(*args).tap { STDERR.puts "[NoBrainer] update_attribute!() is deprecated. Please use update() instead" }
138
+ end
139
+
130
140
  def delete
131
141
  unless @destroyed
132
142
  NoBrainer.run { selector.delete }
@@ -6,7 +6,7 @@ module NoBrainer::Document::Types
6
6
  def add_type_errors
7
7
  return unless @pending_type_errors
8
8
  @pending_type_errors.each do |name, error|
9
- errors.add(name, :invalid_type, :type => error.human_type_name)
9
+ errors.add(name, :invalid_type, error.error)
10
10
  end
11
11
  end
12
12
 
@@ -22,7 +22,10 @@ module NoBrainer::Document::Types
22
22
  module ClassMethods
23
23
  def cast_user_to_model_for(attr, value)
24
24
  type = fields[attr.to_sym].try(:[], :type)
25
- return value if type.nil? || value.nil? || value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic)
25
+ return value if type.nil? || value.nil? ||
26
+ value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic) ||
27
+ value.is_a?(RethinkDB::RQL)
28
+
26
29
  if type.respond_to?(:nobrainer_cast_user_to_model)
27
30
  type.nobrainer_cast_user_to_model(value)
28
31
  else
@@ -30,9 +33,7 @@ module NoBrainer::Document::Types
30
33
  value
31
34
  end
32
35
  rescue NoBrainer::Error::InvalidType => error
33
- error.type = type
34
- error.value = value
35
- error.attr_name = attr
36
+ error.update(:model => self, :value => value, :attr_name => attr, :type => type)
36
37
  raise error
37
38
  end
38
39
 
@@ -62,14 +63,15 @@ module NoBrainer::Document::Types
62
63
 
63
64
  return unless options[:type]
64
65
 
65
- NoBrainer::Document::Types.load_type_extensions(options[:type]) if options[:type]
66
-
66
+ raise "Please use a class for the type option" unless options[:type].is_a?(Class)
67
67
  case options[:type].to_s
68
68
  when "NoBrainer::Geo::Circle" then raise "Cannot store circles :("
69
69
  when "NoBrainer::Geo::Polygon", "NoBrainer::Geo::LineString"
70
70
  raise "Make a request on github if you'd like to store polygons/linestrings"
71
71
  end
72
72
 
73
+ NoBrainer::Document::Types.load_type_extensions(options[:type]) if options[:type]
74
+
73
75
  inject_in_layer :types do
74
76
  define_method("#{attr}=") do |value|
75
77
  begin
@@ -96,11 +98,10 @@ module NoBrainer::Document::Types
96
98
  end
97
99
  end
98
100
 
99
- require File.join(File.dirname(__FILE__), 'types', 'binary')
100
- require File.join(File.dirname(__FILE__), 'types', 'boolean')
101
- Binary = NoBrainer::Binary
102
- Boolean = NoBrainer::Boolean
103
- Geo = NoBrainer::Geo
101
+ %w(binary boolean text geo).each do |type|
102
+ require File.join(File.dirname(__FILE__), 'types', type)
103
+ const_set(type.camelize, NoBrainer.const_get(type.camelize))
104
+ end
104
105
 
105
106
  class << self
106
107
  mattr_accessor :loaded_extensions
@@ -13,10 +13,6 @@ class NoBrainer::Binary
13
13
  else raise InvalidType
14
14
  end
15
15
  end
16
-
17
- def nobrainer_cast_db_to_model(value)
18
- value.is_a?(String) ? RethinkDB::Binary.new(value) : value
19
- end
20
16
  end
21
17
  extend NoBrainerExtentions
22
18
  end
@@ -1,4 +1,3 @@
1
- # We namespace our fake Boolean class to avoid polluting the global namespace
2
1
  class NoBrainer::Boolean
3
2
  def initialize; raise; end
4
3
  def self.inspect; 'Boolean'; end
@@ -0,0 +1 @@
1
+ # Look in lib/no_brainer/geo.rb instead
@@ -7,6 +7,9 @@ class String
7
7
  when String then value
8
8
  when Symbol then value.to_s
9
9
  else raise InvalidType
10
+ end.tap do |str|
11
+ max_length = NoBrainer::Config.max_string_length
12
+ raise InvalidType.new(:error => { :message => :too_long, :count => max_length }) if str.size > max_length
10
13
  end
11
14
  end
12
15
  end
@@ -0,0 +1,18 @@
1
+ class NoBrainer::Text
2
+ def initialize; raise; end
3
+ def self.inspect; 'Text'; end
4
+ def self.to_s; inspect; end
5
+ def self.name; inspect; end
6
+
7
+ module NoBrainerExtentions
8
+ InvalidType = NoBrainer::Error::InvalidType
9
+
10
+ def nobrainer_cast_user_to_model(value)
11
+ case value
12
+ when String then value
13
+ else raise InvalidType
14
+ end
15
+ end
16
+ end
17
+ extend NoBrainerExtentions
18
+ end
@@ -1,8 +1,11 @@
1
1
  module NoBrainer::Document::Validation
2
+ extend NoBrainer::Autoload
2
3
  extend ActiveSupport::Concern
3
4
  include ActiveModel::Validations
4
5
  include ActiveModel::Validations::Callbacks
5
6
 
7
+ autoload_and_include :Uniqueness, :NotNull
8
+
6
9
  included do
7
10
  # We don't want before_validation returning false to halt the chain.
8
11
  define_callbacks :validation, :skip_after_callbacks_if_terminated => true,
@@ -12,7 +15,7 @@ module NoBrainer::Document::Validation
12
15
  def valid?(context=nil, options={})
13
16
  context ||= new_record? ? :create : :update
14
17
 
15
- # copy/pasted, because we need to have control on errors.clear
18
+ # XXX Monkey Patching, because we need to have control on errors.clear
16
19
  current_context, self.validation_context = validation_context, context
17
20
  errors.clear unless options[:clear_errors] == false
18
21
  run_validations!
@@ -20,15 +23,37 @@ module NoBrainer::Document::Validation
20
23
  self.validation_context = current_context
21
24
  end
22
25
 
26
+ SHORTHANDS = { :format => :format, :length => :length, :required => :presence,
27
+ :uniq => :uniqueness, :unique => :uniqueness, :in => :inclusion }
28
+
23
29
  module ClassMethods
24
30
  def _field(attr, options={})
25
31
  super
26
- validates(attr, :format => { :with => options[:format] }) if options.has_key?(:format)
27
- validates(attr, :presence => options[:required]) if options.has_key?(:required)
28
- validates(attr, :uniqueness => options[:unique]) if options.has_key?(:unique)
29
- validates(attr, :uniqueness => options[:uniq]) if options.has_key?(:uniq)
30
- validates(attr, :inclusion => {:in => options[:in]}) if options.has_key?(:in)
32
+
33
+ shorthands = SHORTHANDS
34
+ shorthands = shorthands.merge(:required => :not_null) if options[:type] == NoBrainer::Boolean
35
+ shorthands.each { |k,v| validates(attr, v => options[k]) if options.has_key?(k) }
36
+
31
37
  validates(attr, options[:validates]) if options[:validates]
38
+ validates(attr, :length => { :minimum => options[:min_length] }) if options[:min_length]
39
+ validates(attr, :length => { :maximum => options[:max_length] }) if options[:max_length]
40
+ end
41
+ end
42
+ end
43
+
44
+ class ActiveModel::EachValidator
45
+ def should_validate_field?(record, attribute)
46
+ record.new_record? || record.__send__("#{attribute}_changed?")
47
+ end
48
+
49
+ # XXX Monkey Patching :(
50
+ def validate(record)
51
+ attributes.each do |attribute|
52
+ next unless should_validate_field?(record, attribute) # <--- Added
53
+ value = record.read_attribute_for_validation(attribute)
54
+ next if value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic) # <--- Added
55
+ next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
56
+ validate_each(record, attribute, value)
32
57
  end
33
58
  end
34
59
  end
@@ -0,0 +1,15 @@
1
+ module NoBrainer::Document::Validation::NotNull
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def validates_not_null(*attr_names)
6
+ validates_with(NotNullValidator, _merge_attributes(attr_names))
7
+ end
8
+ end
9
+
10
+ class NotNullValidator < ActiveModel::EachValidator
11
+ def validate_each(doc, attr, value)
12
+ doc.errors.add(attr, :undefined, options) if value.nil?
13
+ end
14
+ end
15
+ end
@@ -1,4 +1,4 @@
1
- module NoBrainer::Document::Uniqueness
1
+ module NoBrainer::Document::Validation::Uniqueness
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def _create(options={})
@@ -27,7 +27,7 @@ module NoBrainer::Document::Uniqueness
27
27
  self.class.unique_validators
28
28
  .map { |validator| validator.attributes.map { |attr| [attr, validator] } }
29
29
  .flatten(1)
30
- .select { |f, validator| validator.should_validate_uniquess_of?(self, f) }
30
+ .select { |f, validator| validator.should_validate_field?(self, f) }
31
31
  .map { |f, options| _lock_key_from_field(f) }
32
32
  .sort
33
33
  .uniq
@@ -51,7 +51,7 @@ module NoBrainer::Document::Uniqueness
51
51
 
52
52
  module ClassMethods
53
53
  def validates_uniqueness_of(*attr_names)
54
- validates_with UniquenessValidator, _merge_attributes(attr_names)
54
+ validates_with(UniquenessValidator, _merge_attributes(attr_names))
55
55
  end
56
56
 
57
57
  def inherited(subclass)
@@ -72,19 +72,20 @@ module NoBrainer::Document::Uniqueness
72
72
  end
73
73
  end
74
74
 
75
- def should_validate_uniquess_of?(doc, field)
76
- (scope + [field]).any? { |f| doc.__send__("#{f}_changed?") }
75
+ def should_validate_field?(doc, field)
76
+ doc.new_record? || (scope + [field]).any? { |f| doc.__send__("#{f}_changed?") }
77
77
  end
78
78
 
79
79
  def validate_each(doc, attr, value)
80
- return true unless should_validate_uniquess_of?(doc, attr)
81
-
82
80
  criteria = doc.root_class.unscoped.where(attr => value)
83
81
  criteria = apply_scopes(criteria, doc)
84
82
  criteria = exclude_doc(criteria, doc) if doc.persisted?
85
- is_unique = criteria.count == 0
86
- doc.errors.add(attr, :taken, options.except(:scope).merge(:value => value)) unless is_unique
87
- is_unique
83
+ doc.errors.add(attr, :taken, options.except(:scope).merge(:value => value)) unless criteria.empty?
84
+ rescue NoBrainer::Error::InvalidType
85
+ # We can't run the uniqueness validator: where() won't accept bad types
86
+ # and we have some values that don't have the right type.
87
+ # Note that it's fine to not add errors because the type validations will
88
+ # prevent the document from being saved.
88
89
  end
89
90
 
90
91
  def apply_scopes(criteria, doc)
@@ -5,25 +5,13 @@ module NoBrainer::Error
5
5
  class ChildrenExist < RuntimeError; end
6
6
  class CannotUseIndex < RuntimeError; end
7
7
  class MissingIndex < RuntimeError; end
8
- class InvalidType < RuntimeError; end
9
8
  class AssociationNotPersisted < RuntimeError; end
10
9
  class ReadonlyField < RuntimeError; end
11
10
  class MissingAttribute < RuntimeError; end
12
11
  class UnknownAttribute < RuntimeError; end
13
12
  class AtomicBlock < RuntimeError; end
14
-
15
- class CannotReadAtomic < RuntimeError
16
- attr_accessor :instance, :field, :value
17
- def initialize(instance, field, value)
18
- @instance = instance
19
- @field = field
20
- @value = value
21
- end
22
-
23
- def message
24
- "Cannot read #{field}, atomic operations are pending"
25
- end
26
- end
13
+ class LostLock < RuntimeError; end
14
+ class LockUnavailable < RuntimeError; end
27
15
 
28
16
  class DocumentInvalid < RuntimeError
29
17
  attr_accessor :instance
@@ -37,23 +25,32 @@ module NoBrainer::Error
37
25
  end
38
26
 
39
27
  class InvalidType < RuntimeError
40
- attr_accessor :attr_name, :value, :type
28
+ attr_accessor :model, :attr_name, :value, :type, :error
41
29
  def initialize(options={})
42
- @attr_name = options[:attr_name]
43
- @value = options[:value]
44
- @type = options[:type]
30
+ update(options)
31
+ end
32
+
33
+ def update(options={})
34
+ options.assert_valid_keys(:model, :attr_name, :type, :value, :error)
35
+ options.each { |k,v| instance_variable_set("@#{k}", v) }
45
36
  end
46
37
 
47
38
  def human_type_name
48
39
  type.to_s.underscore.humanize.downcase
49
40
  end
50
41
 
42
+ def error
43
+ # dup because errors.add eventually .delete() on our option.
44
+ @error.nil? ? (type && { :type => human_type_name }) : @error.dup
45
+ end
46
+
51
47
  def message
52
- if attr_name && type && value
53
- "#{attr_name} should be used with a #{human_type_name}. Got `#{value}` (#{value.class})"
54
- else
55
- super
56
- end
48
+ return super unless model && attr_name && error
49
+ value = self.value
50
+ mock = model.allocate
51
+ mock.singleton_class.send(:define_method, :read_attribute_for_validation) { |_| value }
52
+ mock.errors.add(attr_name, :invalid_type, error)
53
+ mock.errors.full_messages.first
57
54
  end
58
55
  end
59
56