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
@@ -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