nobrainer 0.20.0 → 0.21.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 (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
@@ -21,6 +21,10 @@ class NoBrainer::Document::Association::BelongsTo
21
21
  (options[:class_name] || target_name.to_s.camelize).constantize
22
22
  end
23
23
 
24
+ def base_criteria
25
+ target_model.unscoped
26
+ end
27
+
24
28
  def hook
25
29
  super
26
30
 
@@ -34,11 +38,11 @@ class NoBrainer::Document::Association::BelongsTo
34
38
  owner_model.validates(target_name, options[:validates]) if options[:validates]
35
39
 
36
40
  delegate("#{foreign_key}=", :assign_foreign_key, :call_super => true)
41
+ delegate("#{target_name}_changed?", "#{foreign_key}_changed?", :to => :self)
37
42
  add_callback_for(:after_validation)
38
43
  end
39
44
 
40
- eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ primary_key },
41
- :unscoped => true
45
+ eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ primary_key }
42
46
  end
43
47
 
44
48
  # Note:
@@ -20,12 +20,13 @@ module NoBrainer::Document::Association::Core
20
20
  association_model.new(self, owner)
21
21
  end
22
22
 
23
- def delegate(method_name, target, options={})
23
+ def delegate(method_src, method_dst, options={})
24
24
  metadata = self
25
25
  owner_model.inject_in_layer :associations do
26
- define_method(method_name) do |*args, &block|
26
+ define_method(method_src) do |*args, &block|
27
27
  super(*args, &block) if options[:call_super]
28
- associations[metadata].__send__(target, *args, &block)
28
+ target = options[:to] == :self ? self : associations[metadata]
29
+ target.__send__(method_dst, *args, &block)
29
30
  end
30
31
  end
31
32
  end
@@ -49,7 +50,7 @@ module NoBrainer::Document::Association::Core
49
50
 
50
51
  included { attr_accessor :metadata, :owner }
51
52
 
52
- delegate :primary_key, :foreign_key, :target_name, :target_model, :to => :metadata
53
+ delegate :primary_key, :foreign_key, :target_name, :target_model, :base_criteria, :to => :metadata
53
54
 
54
55
  def initialize(metadata, owner)
55
56
  @metadata, @owner = metadata, owner
@@ -7,9 +7,8 @@ class NoBrainer::Document::Association::EagerLoader
7
7
  owner_key = instance_exec(&options[:owner_key])
8
8
  target_key = instance_exec(&options[:target_key])
9
9
 
10
- criteria = target_model.all
10
+ criteria = base_criteria
11
11
  criteria = criteria.merge(additional_criteria) if additional_criteria
12
- criteria = criteria.unscoped if options[:unscoped]
13
12
 
14
13
  unloaded_docs = docs.reject { |doc| doc.associations[self].loaded? }
15
14
 
@@ -40,20 +39,20 @@ class NoBrainer::Document::Association::EagerLoader
40
39
  association.eager_load(docs, criteria)
41
40
  end
42
41
 
43
- def eager_load(docs, preloads)
44
- case preloads
45
- when Hash then preloads.each do |k,v|
42
+ def eager_load(docs, what)
43
+ case what
44
+ when Hash then what.each do |k,v|
46
45
  if v.is_a?(NoBrainer::Criteria)
47
46
  v = v.dup
48
- nested_preloads, v.options[:preload] = v.options[:preload], []
47
+ nested_preloads, v.options[:eager_load] = v.options[:eager_load], []
49
48
  eager_load(eager_load_association(docs, k, v), nested_preloads)
50
49
  else
51
50
  eager_load(eager_load_association(docs, k), v)
52
51
  end
53
52
  end
54
- when Array then preloads.each { |v| eager_load(docs, v) }
53
+ when Array then what.each { |v| eager_load(docs, v) }
55
54
  when nil then;
56
- else eager_load_association(docs, preloads) # String and Symbol
55
+ else eager_load_association(docs, what) # String and Symbol
57
56
  end
58
57
  true
59
58
  end
@@ -2,7 +2,7 @@ class NoBrainer::Document::Association::HasMany
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent]
5
+ VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
  extend NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
@@ -21,6 +21,10 @@ class NoBrainer::Document::Association::HasMany
21
21
  (options[:class_name] || target_name.to_s.singularize.camelize).constantize
22
22
  end
23
23
 
24
+ def base_criteria
25
+ options[:scope] ? target_model.instance_exec(&options[:scope]) : target_model.all
26
+ end
27
+
24
28
  def inverses
25
29
  # We can always infer the inverse association of a has_many relationship,
26
30
  # because a belongs_to association cannot have a scope applied on the
@@ -37,15 +41,27 @@ class NoBrainer::Document::Association::HasMany
37
41
 
38
42
  def hook
39
43
  super
40
- add_callback_for(:before_destroy) if options[:dependent]
44
+
45
+ if options[:scope]
46
+ raise ":scope must be passed a lambda like this: `:scope => ->{ where(...) }'" unless options[:scope].is_a?(Proc)
47
+ raise ":dependent and :scope cannot be used together" if options[:dependent]
48
+ end
49
+
50
+ if options[:dependent]
51
+ unless [:destroy, :delete, :nullify, :restrict, nil].include?(options[:dependent])
52
+ raise "Invalid dependent option: `#{options[:dependent].inspect}'. " +
53
+ "Valid options are: :destroy, :delete, :nullify, or :restrict"
54
+ end
55
+ add_callback_for(:before_destroy)
56
+ end
41
57
  end
42
58
 
43
59
  eager_load_with :owner_key => ->{ primary_key }, :target_key => ->{ foreign_key }
44
60
  end
45
61
 
46
62
  def target_criteria
47
- @target_criteria ||= target_model.where(foreign_key => owner.pk_value)
48
- .after_find(set_inverse_proc)
63
+ @target_criteria ||= base_criteria.where(foreign_key => owner.pk_value)
64
+ .after_find(set_inverse_proc)
49
65
  end
50
66
 
51
67
  def read
@@ -53,8 +69,8 @@ class NoBrainer::Document::Association::HasMany
53
69
  end
54
70
 
55
71
  def write(new_children)
56
- raise "You can't assign #{target_name}. " \
57
- "Instead, you must modify delete and create #{target_model} manually."
72
+ raise "You can't assign `#{target_name}'. " \
73
+ "Instead, you must modify delete and create `#{target_model}' manually."
58
74
  end
59
75
 
60
76
  def loaded?
@@ -86,12 +102,10 @@ class NoBrainer::Document::Association::HasMany
86
102
  def before_destroy_callback
87
103
  criteria = target_criteria.unscoped.without_cache
88
104
  case metadata.options[:dependent]
89
- when nil then
90
105
  when :destroy then criteria.destroy_all
91
106
  when :delete then criteria.delete_all
92
107
  when :nullify then criteria.update_all(foreign_key => nil)
93
108
  when :restrict then raise NoBrainer::Error::ChildrenExist unless criteria.count.zero?
94
- else raise "Unrecognized dependent option: #{metadata.options[:dependent]}"
95
109
  end
96
110
  end
97
111
  end
@@ -2,7 +2,7 @@ class NoBrainer::Document::Association::HasManyThrough
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:through]
5
+ VALID_OPTIONS = [:through, :scope]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
 
8
8
  def through_association_name
@@ -14,10 +14,18 @@ class NoBrainer::Document::Association::HasManyThrough
14
14
  raise "#{through_association_name} association not found"
15
15
  end
16
16
 
17
- def eager_load(docs, criteria=nil)
17
+ def eager_load(docs, additional_criteria=nil)
18
+ criteria = target_model.instance_exec(&options[:scope]) if options[:scope]
19
+ criteria = criteria ? criteria.merge(additional_criteria) : additional_criteria if additional_criteria
18
20
  NoBrainer::Document::Association::EagerLoader.new
19
21
  .eager_load_association(through_association.eager_load(docs), target_name, criteria)
20
22
  end
23
+
24
+ def target_model
25
+ meta = through_association.target_model.association_metadata
26
+ association = meta[target_name.to_sym] || meta[target_name.to_s.singularize.to_sym]
27
+ association.target_model
28
+ end
21
29
  end
22
30
 
23
31
  def read
@@ -26,6 +34,7 @@ class NoBrainer::Document::Association::HasManyThrough
26
34
  end
27
35
 
28
36
  def write(new_children)
29
- raise "You can't assign #{target_name}"
37
+ raise "You can't assign `#{target_name}'. " \
38
+ "Instead, you must modify delete and create `#{target_model}' manually."
30
39
  end
31
40
  end
@@ -2,22 +2,38 @@ module NoBrainer::Document::AtomicOps
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  class PendingAtomic
5
+ attr_accessor :type
6
+
5
7
  def self._new(instance, field, value, is_user_value)
6
- case value
7
- when Array then PendingAtomicArray
8
- when Set then PendingAtomicSet
8
+ type = instance.class.fields[field.to_sym].try(:[], :type)
9
+ type ||= value.class unless value.nil?
10
+
11
+ case
12
+ when type == Array then PendingAtomicArray
13
+ when type == Set then PendingAtomicSet
9
14
  else self
10
- end.new(instance, field, value, is_user_value)
15
+ end.new(instance, field, value, is_user_value, type)
11
16
  end
12
17
 
13
- def initialize(instance, field, value, is_user_value)
18
+ def initialize(instance, field, value, is_user_value, type)
14
19
  @instance = instance
15
20
  @field = field.to_s
16
21
  @value = value
17
22
  @is_user_value = is_user_value
23
+ @type = type
18
24
  @ops = []
19
25
  end
20
26
 
27
+ def default_value
28
+ case
29
+ when @type == Array then []
30
+ when @type == Set then []
31
+ when @type == Integer then 0
32
+ when @type == Float then 0.0
33
+ when @type == String then ""
34
+ end
35
+ end
36
+
21
37
  def initialize_copy(other)
22
38
  super
23
39
  @ops = @ops.dup
@@ -29,6 +45,11 @@ module NoBrainer::Document::AtomicOps
29
45
  alias_method :inspect, :to_s
30
46
 
31
47
  def method_missing(method, *a, &b)
48
+ if method == :<<
49
+ method = :append
50
+ modify_source!
51
+ end
52
+
32
53
  @ops << [method, a, b]
33
54
  self
34
55
  end
@@ -36,11 +57,10 @@ module NoBrainer::Document::AtomicOps
36
57
  def compile_rql_value(rql_doc)
37
58
  field = @instance.class.lookup_field_alias(@field)
38
59
  value = @is_user_value ? RethinkDB::RQL.new.expr(@value) : rql_doc[field]
60
+ value = value.default(default_value) if default_value
39
61
  @ops.reduce(value) { |v, (method, a, b)| v.__send__(method, *a, &b) }
40
62
  end
41
- end
42
63
 
43
- class PendingAtomicContainer < PendingAtomic
44
64
  def modify_source!
45
65
  unless @instance._is_attribute_touched?(@field)
46
66
  @instance.write_attribute(@field, self)
@@ -48,56 +68,59 @@ module NoBrainer::Document::AtomicOps
48
68
  end
49
69
  end
50
70
 
51
- class PendingAtomicArray < PendingAtomicContainer
52
- def -(value)
53
- @ops << [:difference, [value.to_a]]
71
+ class PendingAtomicContainer < PendingAtomic
72
+ def &(value)
73
+ @ops << [:set_intersection, [value.to_a]]
54
74
  self
55
75
  end
56
- def difference(v); self - v; end
76
+
77
+ def |(value)
78
+ @ops << [:set_union, [value.to_a]]
79
+ self
80
+ end
81
+
82
+ def add(v); self + v; end
83
+ def difference(v); self - v; end
84
+ def intersection(v); self & v; end
85
+ def union(v); self | v; end
57
86
 
58
87
  def delete(value)
59
88
  difference([value])
60
89
  end
90
+ end
61
91
 
62
- def +(value)
63
- @ops << [:+, [value.to_a]]
92
+ class PendingAtomicSet < PendingAtomicContainer
93
+ def <<(value)
94
+ @ops << [:set_union, [[value]]]
95
+ modify_source!
64
96
  self
65
97
  end
66
- def add(v); self + v; end
67
98
 
68
- def &(value)
69
- @ops << [:set_intersection, [value.to_a]]
99
+ def +(value)
100
+ @ops << [:set_union, [value.to_a]]
70
101
  self
71
102
  end
72
- def intersection(v); self & v; end
73
103
 
74
- def |(value)
75
- @ops << [:set_union, [value.to_a]]
104
+ def -(value)
105
+ @ops << [:set_difference, [value.to_a]]
76
106
  self
77
107
  end
78
- def union(v); self | v; end
108
+ end
79
109
 
110
+ class PendingAtomicArray < PendingAtomicContainer
80
111
  def <<(value)
81
112
  @ops << [:append, [value]]
82
113
  modify_source!
83
114
  self
84
115
  end
85
- end
86
-
87
- class PendingAtomicSet < PendingAtomicContainer
88
- def -(value)
89
- @ops << [:set_difference, [value.to_a]]
90
- self
91
- end
92
116
 
93
117
  def +(value)
94
- @ops << [:set_union, [value.to_a]]
118
+ @ops << [:+, [value.to_a]]
95
119
  self
96
120
  end
97
121
 
98
- def <<(value)
99
- @ops << [:set_union, [[value]]]
100
- modify_source!
122
+ def -(value)
123
+ @ops << [:difference, [value.to_a]]
101
124
  self
102
125
  end
103
126
  end
@@ -108,6 +131,9 @@ module NoBrainer::Document::AtomicOps
108
131
  end
109
132
 
110
133
  def _touch_attribute(name)
134
+ # The difference with dirty tracking and this is that dirty tracking does
135
+ # not take into account fields that are set with their old value, whereas the
136
+ # touched attribute does.
111
137
  @_touched_attributes << name.to_s
112
138
  end
113
139
 
@@ -142,12 +168,12 @@ module NoBrainer::Document::AtomicOps
142
168
  def _read_attribute(name)
143
169
  ensure_exclusive_atomic!
144
170
  value = super
171
+ return value unless in_atomic?
145
172
 
146
- case [in_atomic?, value.is_a?(PendingAtomic)]
147
- when [true, false] then PendingAtomic._new(self, name, value, _is_attribute_touched?(name))
148
- when [false, true] then raise NoBrainer::Error::CannotReadAtomic.new(self, name, value)
149
- when [true, true] then value.is_a?(PendingAtomicContainer) ? value : value.dup
150
- when [false, false] then value
173
+ case value
174
+ when PendingAtomicContainer then value
175
+ when PendingAtomic then value.dup
176
+ else PendingAtomic._new(self, name, value, _is_attribute_touched?(name))
151
177
  end
152
178
  end
153
179
 
@@ -174,39 +200,15 @@ module NoBrainer::Document::AtomicOps
174
200
  if saved
175
201
  @_attributes.each do |attr, value|
176
202
  next unless value.is_a?(PendingAtomic)
177
- @_attributes[attr] = value.class.new(self, attr, nil, false)
203
+ @_attributes[attr] = value.class.new(self, attr, nil, false, value.type)
178
204
  end
179
205
  end
180
206
  end
181
207
  end
182
208
 
183
- def read_attribute_for_change(attr)
184
- super
185
- rescue NoBrainer::Error::CannotReadAtomic => e
186
- e.value
187
- end
188
-
189
- def read_attribute_for_validation(attr)
190
- super
191
- rescue NoBrainer::Error::CannotReadAtomic => e
192
- e.value
193
- end
194
-
195
209
  module ClassMethods
196
210
  def persistable_value(k, v, options={})
197
211
  v.is_a?(PendingAtomic) ? v.compile_rql_value(options[:rql_doc]) : super
198
212
  end
199
213
  end
200
214
  end
201
-
202
- class ActiveModel::EachValidator
203
- # XXX Monkey Patching :(
204
- def validate(record)
205
- attributes.each do |attribute|
206
- value = record.read_attribute_for_validation(attribute)
207
- next if value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic) # <--- This is the added line
208
- next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
209
- validate_each(record, attribute, value)
210
- end
211
- end
212
- end
@@ -1,9 +1,10 @@
1
1
  module NoBrainer::Document::Attributes
2
2
  VALID_FIELD_OPTIONS = [:index, :default, :type, :readonly, :primary_key, :lazy_fetch, :store_as,
3
- :validates, :required, :unique, :uniq, :format, :in]
3
+ :validates, :required, :unique, :uniq, :format, :in, :length, :min_length, :max_length]
4
4
  RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] \
5
5
  + NoBrainer::Criteria::Where::OPERATORS
6
6
  extend ActiveSupport::Concern
7
+ include ActiveModel::ForbiddenAttributesProtection
7
8
 
8
9
  included do
9
10
  singleton_class.send(:attr_accessor, :fields)
@@ -23,6 +24,10 @@ module NoBrainer::Document::Attributes
23
24
  Hash[readable_attributes.map { |k| [k, read_attribute(k)] }].with_indifferent_access.freeze
24
25
  end
25
26
 
27
+ def raw_attributes
28
+ @_attributes
29
+ end
30
+
26
31
  def _read_attribute(name)
27
32
  @_attributes[name]
28
33
  end
@@ -54,12 +59,14 @@ module NoBrainer::Document::Attributes
54
59
  end
55
60
 
56
61
  default_value = field_options[:default]
57
- default_value = default_value.call if default_value.is_a?(Proc)
62
+ default_value = instance_exec(&default_value) if default_value.is_a?(Proc)
58
63
  self.write_attribute(name, default_value)
59
64
  end
60
65
  end
61
66
 
62
67
  def assign_attributes(attrs, options={})
68
+ raise ArgumentError, "To assign attributes, please pass a hash instead of `#{attrs.class}'" unless attrs.is_a?(Hash)
69
+
63
70
  if options[:pristine]
64
71
  if options[:keep_ivars] && options[:missing_attributes].try(:[], :pluck)
65
72
  options[:missing_attributes][:pluck].keys.each { |k| @_attributes.delete(k) }
@@ -74,6 +81,7 @@ module NoBrainer::Document::Attributes
74
81
  clear_dirtiness(options)
75
82
  else
76
83
  clear_dirtiness(options) if options[:pristine]
84
+ attrs = sanitize_for_mass_assignment(attrs)
77
85
  attrs.each { |k,v| self.write_attribute(k,v) }
78
86
  end
79
87
  assign_defaults(options) if options[:pristine]
@@ -82,7 +90,7 @@ module NoBrainer::Document::Attributes
82
90
 
83
91
  def inspectable_attributes
84
92
  # TODO test that thing
85
- Hash[@_attributes.sort_by { |k,v| self.class.fields.keys.index(k.to_sym) || 2**10 }]
93
+ Hash[@_attributes.sort_by { |k,v| self.class.fields.keys.index(k.to_sym) || 2**10 }].with_indifferent_access.freeze
86
94
  end
87
95
 
88
96
  def to_s