nobrainer 0.22.0 → 0.28.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +1 -1
  3. data/lib/no_brainer/config.rb +54 -14
  4. data/lib/no_brainer/connection.rb +24 -21
  5. data/lib/no_brainer/connection_manager.rb +29 -9
  6. data/lib/no_brainer/criteria/cache.rb +8 -0
  7. data/lib/no_brainer/criteria/changes.rb +16 -0
  8. data/lib/no_brainer/criteria/core.rb +4 -5
  9. data/lib/no_brainer/criteria/eager_load.rb +2 -2
  10. data/lib/no_brainer/criteria/enumerable.rb +3 -1
  11. data/lib/no_brainer/criteria/find.rb +6 -9
  12. data/lib/no_brainer/criteria/first.rb +3 -3
  13. data/lib/no_brainer/criteria/first_or_create.rb +114 -0
  14. data/lib/no_brainer/criteria/join.rb +62 -0
  15. data/lib/no_brainer/criteria/order_by.rb +19 -16
  16. data/lib/no_brainer/criteria/run.rb +26 -0
  17. data/lib/no_brainer/criteria/scope.rb +8 -8
  18. data/lib/no_brainer/criteria/where.rb +130 -107
  19. data/lib/no_brainer/criteria.rb +4 -4
  20. data/lib/no_brainer/document/association/belongs_to.rb +44 -17
  21. data/lib/no_brainer/document/association/core.rb +11 -0
  22. data/lib/no_brainer/document/association/eager_loader.rb +26 -26
  23. data/lib/no_brainer/document/association/has_many.rb +7 -9
  24. data/lib/no_brainer/document/association/has_many_through.rb +1 -2
  25. data/lib/no_brainer/document/association/has_one.rb +5 -1
  26. data/lib/no_brainer/document/association.rb +5 -5
  27. data/lib/no_brainer/document/atomic_ops.rb +40 -7
  28. data/lib/no_brainer/document/attributes.rb +24 -15
  29. data/lib/no_brainer/document/callbacks.rb +1 -1
  30. data/lib/no_brainer/document/core.rb +18 -18
  31. data/lib/no_brainer/document/criteria.rb +9 -5
  32. data/lib/no_brainer/document/dirty.rb +15 -12
  33. data/lib/no_brainer/document/dynamic_attributes.rb +6 -0
  34. data/lib/no_brainer/document/index/index.rb +5 -9
  35. data/lib/no_brainer/document/index/meta_store.rb +1 -10
  36. data/lib/no_brainer/document/index/synchronizer.rb +16 -20
  37. data/lib/no_brainer/document/index.rb +3 -3
  38. data/lib/no_brainer/document/lazy_fetch.rb +3 -3
  39. data/lib/no_brainer/document/missing_attributes.rb +7 -2
  40. data/lib/no_brainer/document/persistance.rb +14 -30
  41. data/lib/no_brainer/document/polymorphic.rb +8 -4
  42. data/lib/no_brainer/document/primary_key/generator.rb +6 -1
  43. data/lib/no_brainer/document/primary_key.rb +19 -4
  44. data/lib/no_brainer/document/serialization.rb +0 -2
  45. data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
  46. data/lib/no_brainer/document/table_config.rb +118 -0
  47. data/lib/no_brainer/document/timestamps.rb +8 -0
  48. data/lib/no_brainer/document/validation/core.rb +63 -0
  49. data/lib/no_brainer/document/validation/uniqueness.rb +30 -36
  50. data/lib/no_brainer/document/validation.rb +1 -58
  51. data/lib/no_brainer/document.rb +2 -2
  52. data/lib/no_brainer/error.rb +1 -0
  53. data/lib/no_brainer/geo/base.rb +0 -1
  54. data/lib/no_brainer/locale/en.yml +1 -0
  55. data/lib/no_brainer/lock.rb +12 -8
  56. data/lib/no_brainer/profiler/controller_runtime.rb +76 -0
  57. data/lib/no_brainer/{query_runner → profiler}/logger.rb +11 -27
  58. data/lib/no_brainer/profiler.rb +11 -0
  59. data/lib/no_brainer/query_runner/database_on_demand.rb +8 -8
  60. data/lib/no_brainer/query_runner/driver.rb +3 -1
  61. data/lib/no_brainer/query_runner/missing_index.rb +3 -3
  62. data/lib/no_brainer/query_runner/profiler.rb +43 -0
  63. data/lib/no_brainer/query_runner/reconnect.rb +38 -23
  64. data/lib/no_brainer/query_runner/run_options.rb +35 -15
  65. data/lib/no_brainer/query_runner/table_on_demand.rb +18 -11
  66. data/lib/no_brainer/query_runner.rb +3 -3
  67. data/lib/no_brainer/railtie/database.rake +14 -4
  68. data/lib/no_brainer/railtie.rb +5 -12
  69. data/lib/no_brainer/rql.rb +11 -8
  70. data/lib/no_brainer/symbol_decoration.rb +11 -0
  71. data/lib/no_brainer/system/cluster_config.rb +5 -0
  72. data/lib/no_brainer/system/db_config.rb +5 -0
  73. data/lib/no_brainer/system/document.rb +24 -0
  74. data/lib/no_brainer/system/issue.rb +10 -0
  75. data/lib/no_brainer/system/job.rb +10 -0
  76. data/lib/no_brainer/system/log.rb +11 -0
  77. data/lib/no_brainer/system/server_config.rb +7 -0
  78. data/lib/no_brainer/system/server_status.rb +9 -0
  79. data/lib/no_brainer/system/stat.rb +11 -0
  80. data/lib/no_brainer/system/table_config.rb +10 -0
  81. data/lib/no_brainer/system/table_status.rb +8 -0
  82. data/lib/no_brainer/system.rb +17 -0
  83. data/lib/nobrainer.rb +16 -11
  84. data/lib/rails/generators/nobrainer/install_generator.rb +48 -0
  85. data/lib/rails/generators/nobrainer/{model/model_generator.rb → model_generator.rb} +7 -3
  86. data/lib/rails/generators/nobrainer/namespace_fix.rb +15 -0
  87. data/lib/rails/generators/{nobrainer/model/templates/model.rb.tt → templates/model.rb} +1 -1
  88. data/lib/rails/generators/templates/nobrainer.rb +101 -0
  89. metadata +34 -10
  90. data/lib/no_brainer/document/store_in.rb +0 -35
  91. data/lib/rails/generators/nobrainer.rb +0 -18
@@ -4,45 +4,72 @@ class NoBrainer::Document::Association::BelongsTo
4
4
  class Metadata
5
5
  VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :foreign_key_store_as, :index, :validates, :required]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
- extend NoBrainer::Document::Association::EagerLoader::Generic
7
+ include NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
9
9
  def foreign_key
10
- # TODO test :foreign_key
11
10
  options[:foreign_key].try(:to_sym) || :"#{target_name}_#{primary_key}"
12
11
  end
13
12
 
14
13
  def primary_key
15
- # TODO test :primary_key
16
- options[:primary_key].try(:to_sym) || target_model.pk_name
14
+ # We default the primary_key to `:id' and not `target_model.pk_name',
15
+ # because we don't want to require the target_model to be already loaded.
16
+ # (We want the ability to load models in any order).
17
+ # Using target_model.pk_name and allowing lazy loading of models is
18
+ # difficult due to the inexistant API to remove validations if the
19
+ # foreign_key name was to be changed as the pk_name gets renamed.
20
+ return options[:primary_key].to_sym if options[:primary_key]
21
+
22
+ NoBrainer::Document::PrimaryKey::DEFAULT_PK_NAME.tap do |default_pk_name|
23
+ # We'll try to give a warning when we see a different target pk name (best effort).
24
+ real_pk_name = target_model.pk_name rescue nil
25
+ if real_pk_name && real_pk_name != default_pk_name
26
+ raise "Please specify the primary_key name on the following belongs_to association as such:\n" +
27
+ " belongs_to :#{target_name}, :primary_key => :#{real_pk_name}"
28
+ end
29
+ end
17
30
  end
18
31
 
19
32
  def target_model
20
- # TODO test :class_name
21
- (options[:class_name] || target_name.to_s.camelize).constantize
33
+ get_model_by_name(options[:class_name] || target_name.to_s.camelize)
22
34
  end
23
35
 
24
36
  def base_criteria
25
- target_model.unscoped
37
+ target_model.without_ordering.unscoped
26
38
  end
27
39
 
28
40
  def hook
29
41
  super
30
42
 
31
- # TODO It would be good to set the type we want to work with, but because
32
- # the target class is eager loaded, we are not doing it.
33
- # This would have the effect of loading all the models because they
34
- # are likely to be related to each other. So we don't know the type
35
- # of the primary key of the target.
43
+ # TODO set the type of the foreign key to be the same as the target's primary key
44
+ if owner_model.association_metadata.values.any? { |assoc|
45
+ assoc.is_a?(self.class) && assoc != self && assoc.foreign_key == foreign_key }
46
+ raise "Cannot declare `#{target_name}' in #{owner_model}: the foreign_key `#{foreign_key}' is already used"
47
+ end
48
+
36
49
  owner_model.field(foreign_key, :store_as => options[:foreign_key_store_as], :index => options[:index])
37
- owner_model.validates(target_name, :presence => options[:required]) if options[:required]
38
- owner_model.validates(target_name, options[:validates]) if options[:validates]
50
+
51
+ unless options[:validates] == false
52
+ owner_model.validates(target_name, options[:validates]) if options[:validates]
53
+
54
+ if options[:required]
55
+ owner_model.validates(target_name, :presence => options[:required])
56
+ else
57
+ # Always validate the validity of the foreign_key if not nil.
58
+ owner_model.validates_each(foreign_key) do |doc, attr, value|
59
+ if !value.nil? && doc.read_attribute_for_validation(target_name).nil?
60
+ doc.errors.add(attr, :invalid_foreign_key, :target_model => target_model, :primary_key => primary_key)
61
+ end
62
+ end
63
+ end
64
+ end
39
65
 
40
66
  delegate("#{foreign_key}=", :assign_foreign_key, :call_super => true)
41
67
  delegate("#{target_name}_changed?", "#{foreign_key}_changed?", :to => :self)
42
68
  add_callback_for(:after_validation)
43
69
  end
44
70
 
45
- eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ primary_key }
71
+ def eager_load_owner_key; foreign_key; end
72
+ def eager_load_target_key; primary_key; end
46
73
  end
47
74
 
48
75
  # Note:
@@ -58,13 +85,13 @@ class NoBrainer::Document::Association::BelongsTo
58
85
  return target if loaded?
59
86
 
60
87
  if fk = owner.read_attribute(foreign_key)
61
- preload(target_model.unscoped.where(primary_key => fk).first)
88
+ preload(base_criteria.where(primary_key => fk).first)
62
89
  end
63
90
  end
64
91
 
65
92
  def write(target)
66
93
  assert_target_type(target)
67
- owner.write_attribute(foreign_key, target.try(:pk_value))
94
+ owner.write_attribute(foreign_key, target.try(primary_key))
68
95
  preload(target)
69
96
  end
70
97
 
@@ -46,6 +46,17 @@ module NoBrainer::Document::Association::Core
46
46
  end
47
47
  RUBY
48
48
  end
49
+
50
+ def get_model_by_name(model_name)
51
+ return model_name if model_name.is_a?(Module)
52
+
53
+ model_name = model_name.to_s
54
+ current_module = @owner_model.parent
55
+ return model_name.constantize if current_module == Object
56
+ return model_name.constantize if model_name =~ /^::/
57
+ return model_name.constantize if !current_module.const_defined?(model_name)
58
+ current_module.const_get(model_name)
59
+ end
49
60
  end
50
61
 
51
62
  included { attr_accessor :metadata, :owner }
@@ -1,31 +1,32 @@
1
- class NoBrainer::Document::Association::EagerLoader
1
+ module NoBrainer::Document::Association::EagerLoader
2
+ extend self
3
+
2
4
  module Generic
3
5
  # Used in associations to declare generic eager loading capabilities
4
- # The association should implement loaded? and preload.
5
- def eager_load_with(options={})
6
- define_method(:eager_load) do |docs, additional_criteria=nil|
7
- owner_key = instance_exec(&options[:owner_key])
8
- target_key = instance_exec(&options[:target_key])
9
-
10
- criteria = base_criteria
11
- criteria = criteria.merge(additional_criteria) if additional_criteria
12
-
13
- unloaded_docs = docs.reject { |doc| doc.associations[self].loaded? }
14
-
15
- owner_keys = unloaded_docs.map(&owner_key).compact.uniq
16
- if owner_keys.present?
17
- targets = criteria.where(target_key.in => owner_keys)
18
- .map { |target| [target.read_attribute(target_key), target] }
19
- .each_with_object(Hash.new { |k,v| k[v] = [] }) { |(k,v),h| h[k] << v }
20
-
21
- unloaded_docs.each do |doc|
22
- doc_targets = targets[doc.read_attribute(owner_key)]
23
- doc.associations[self].preload(doc_targets)
24
- end
25
- end
6
+ # The association should implement loaded?, preload,
7
+ # eager_load_owner_key and eager_load_target_key.
8
+ def eager_load(docs, additional_criteria=nil)
9
+ owner_key = eager_load_owner_key
10
+ target_key = eager_load_target_key
11
+
12
+ criteria = base_criteria
13
+ criteria = criteria.merge(additional_criteria) if additional_criteria
14
+
15
+ unloaded_docs = docs.reject { |doc| doc.associations[self].loaded? }
26
16
 
27
- docs.map { |doc| doc.associations[self].read }.flatten.compact.uniq
17
+ owner_keys = unloaded_docs.map(&owner_key).compact.uniq
18
+ if owner_keys.present?
19
+ targets = criteria.where(target_key.in => owner_keys)
20
+ .map { |target| [target.read_attribute(target_key), target] }
21
+ .each_with_object(Hash.new { |k,v| k[v] = [] }) { |(k,v),h| h[k] << v }
22
+
23
+ unloaded_docs.each do |doc|
24
+ doc_targets = targets[doc.read_attribute(owner_key)]
25
+ doc.associations[self].preload(doc_targets)
26
+ end
28
27
  end
28
+
29
+ docs.map { |doc| doc.associations[self].read }.flatten.compact.uniq
29
30
  end
30
31
  end
31
32
 
@@ -33,7 +34,6 @@ class NoBrainer::Document::Association::EagerLoader
33
34
  docs = docs.compact
34
35
  return [] if docs.empty?
35
36
  meta = docs.first.root_class.association_metadata
36
- # TODO test the singularize thingy.
37
37
  association = meta[association_name.to_sym] || meta[association_name.to_s.singularize.to_sym]
38
38
  raise "Unknown association #{association_name}" unless association
39
39
  association.eager_load(docs, criteria)
@@ -41,7 +41,7 @@ class NoBrainer::Document::Association::EagerLoader
41
41
 
42
42
  def eager_load(docs, what)
43
43
  case what
44
- when Hash then what.each do |k,v|
44
+ when Hash then what.each do |k,v|
45
45
  if v.is_a?(NoBrainer::Criteria)
46
46
  v = v.dup
47
47
  nested_preloads, v.options[:eager_load] = v.options[:eager_load], []
@@ -4,21 +4,18 @@ class NoBrainer::Document::Association::HasMany
4
4
  class Metadata
5
5
  VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
- extend NoBrainer::Document::Association::EagerLoader::Generic
7
+ include NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
9
9
  def foreign_key
10
- # TODO test :foreign_key
11
- options[:foreign_key].try(:to_sym) || :"#{owner_model.name.underscore}_#{owner_model.pk_name}"
10
+ options[:foreign_key].try(:to_sym) || :"#{owner_model.name.split('::').last.underscore}_#{primary_key}"
12
11
  end
13
12
 
14
13
  def primary_key
15
- # TODO test :primary_key
16
- options[:primary_key].try(:to_sym) || target_model.pk_name
14
+ options[:primary_key].try(:to_sym) || owner_model.pk_name
17
15
  end
18
16
 
19
17
  def target_model
20
- # TODO test :class_name
21
- (options[:class_name] || target_name.to_s.singularize.camelize).constantize
18
+ get_model_by_name(options[:class_name] || target_name.to_s.singularize.camelize)
22
19
  end
23
20
 
24
21
  def base_criteria
@@ -56,11 +53,12 @@ class NoBrainer::Document::Association::HasMany
56
53
  end
57
54
  end
58
55
 
59
- eager_load_with :owner_key => ->{ primary_key }, :target_key => ->{ foreign_key }
56
+ def eager_load_owner_key; primary_key; end
57
+ def eager_load_target_key; foreign_key; end
60
58
  end
61
59
 
62
60
  def target_criteria
63
- @target_criteria ||= base_criteria.where(foreign_key => owner.pk_value)
61
+ @target_criteria ||= base_criteria.where(foreign_key => owner.__send__(primary_key))
64
62
  .after_find(set_inverse_proc)
65
63
  end
66
64
 
@@ -17,7 +17,7 @@ class NoBrainer::Document::Association::HasManyThrough
17
17
  def eager_load(docs, additional_criteria=nil)
18
18
  criteria = target_model.instance_exec(&options[:scope]) if options[:scope]
19
19
  criteria = criteria ? criteria.merge(additional_criteria) : additional_criteria if additional_criteria
20
- NoBrainer::Document::Association::EagerLoader.new
20
+ NoBrainer::Document::Association::EagerLoader
21
21
  .eager_load_association(through_association.eager_load(docs), target_name, criteria)
22
22
  end
23
23
 
@@ -29,7 +29,6 @@ class NoBrainer::Document::Association::HasManyThrough
29
29
  end
30
30
 
31
31
  def read
32
- # TODO implement joins
33
32
  @targets ||= metadata.eager_load([owner]).freeze
34
33
  end
35
34
 
@@ -1,7 +1,11 @@
1
1
  class NoBrainer::Document::Association::HasOne < NoBrainer::Document::Association::HasMany
2
2
  class Metadata < NoBrainer::Document::Association::HasMany::Metadata
3
3
  def target_model
4
- (options[:class_name] || target_name.to_s.camelize).constantize
4
+ get_model_by_name(options[:class_name] || target_name.to_s.camelize)
5
+ end
6
+
7
+ def base_criteria
8
+ super.without_ordering
5
9
  end
6
10
  end
7
11
 
@@ -28,11 +28,11 @@ module NoBrainer::Document::Association
28
28
  raise "Cannot change the :through option" unless r.options[:through] == options[:through]
29
29
  r.options.merge!(options)
30
30
  else
31
- model_name = (options[:through] ? "#{association}_through" : association.to_s).camelize
32
- metadata_model = NoBrainer::Document::Association.const_get(model_name).const_get(:Metadata)
33
- r = metadata_model.new(self, target, options)
34
- ([self] + descendants).each do |model|
35
- model.association_metadata[target] = r
31
+ class_name = (options[:through] ? "#{association}_through" : association.to_s).camelize
32
+ metadata_class = NoBrainer::Document::Association.const_get(class_name).const_get(:Metadata)
33
+ r = metadata_class.new(self, target, options)
34
+ subclass_tree.each do |subclass|
35
+ subclass.association_metadata[target] = r
36
36
  end
37
37
  end
38
38
  r.hook
@@ -42,7 +42,7 @@ module NoBrainer::Document::AtomicOps
42
42
  def to_s
43
43
  "<`#{@field}' with #{@ops.size} pending atomic operations>"
44
44
  end
45
- alias_method :inspect, :to_s
45
+ def inspect; to_s; end
46
46
 
47
47
  def method_missing(method, *a, &b)
48
48
  if method == :<<
@@ -56,14 +56,20 @@ module NoBrainer::Document::AtomicOps
56
56
 
57
57
  def compile_rql_value(rql_doc)
58
58
  field = @instance.class.lookup_field_alias(@field)
59
- value = @is_user_value ? RethinkDB::RQL.new.expr(@value) : rql_doc[field]
59
+ if @is_user_value
60
+ casted_value = @instance.class.cast_model_to_db_for(@field, @value)
61
+ value = RethinkDB::RQL.new.expr(casted_value)
62
+ else
63
+ value = rql_doc[field]
64
+ end
60
65
  value = value.default(default_value) if default_value
61
66
  @ops.reduce(value) { |v, (method, a, b)| v.__send__(method, *a, &b) }
62
67
  end
63
68
 
64
69
  def modify_source!
65
- unless @instance._is_attribute_touched?(@field)
66
- @instance.write_attribute(@field, self)
70
+ if (@is_user_value && @instance.instance_eval { @_attributes[@field].equal?(@value) }) ||
71
+ !@instance._is_attribute_touched?(@field)
72
+ @instance._write_attribute(@field, self)
67
73
  end
68
74
  end
69
75
  end
@@ -125,6 +131,18 @@ module NoBrainer::Document::AtomicOps
125
131
  end
126
132
  end
127
133
 
134
+ class PendingAtomicUnset < PendingAtomic
135
+ def to_s
136
+ "<unset `#{@field}'>"
137
+ end
138
+
139
+ undef_method(:method_missing)
140
+
141
+ def compile_rql_value(rql_doc)
142
+ RethinkDB::RQL.new.literal()
143
+ end
144
+ end
145
+
128
146
  def clear_dirtiness(options={})
129
147
  super
130
148
  @_touched_attributes = Set.new
@@ -166,13 +184,18 @@ module NoBrainer::Document::AtomicOps
166
184
  end
167
185
 
168
186
  def _read_attribute(name)
187
+ return super if name == self.class.pk_name
188
+
169
189
  ensure_exclusive_atomic!
170
- value = super
171
- return value unless in_atomic?
190
+ return super unless in_atomic?
191
+
192
+ # If we are missing fields, it's okay, we'll assume nil.
193
+ value = missing_field?(name) ? nil : super
172
194
 
173
195
  case value
174
196
  when PendingAtomicContainer then value
175
- when PendingAtomic then value.dup
197
+ when PendingAtomicUnset then raise "Attribute `#{name}' is unset"
198
+ when PendingAtomic then value.dup
176
199
  else PendingAtomic._new(self, name, value, _is_attribute_touched?(name))
177
200
  end
178
201
  end
@@ -193,6 +216,11 @@ module NoBrainer::Document::AtomicOps
193
216
  super
194
217
  end
195
218
 
219
+ def unset(attr)
220
+ return queue_atomic { unset(attr) } unless in_atomic?
221
+ _write_attribute(attr, PendingAtomicUnset.new(self, attr, nil, true, nil))
222
+ end
223
+
196
224
  def save?(options={})
197
225
  # TODO allow reload => true as an option to save+reload in a single op.
198
226
  raise NoBrainer::Error::AtomicBlock.new('You may persist documents only outside of queue_atomic blocks') if in_atomic?
@@ -206,6 +234,11 @@ module NoBrainer::Document::AtomicOps
206
234
  end
207
235
  end
208
236
 
237
+ def reload(*)
238
+ raise NoBrainer::Error::AtomicBlock.new('You may not reload fields within an atomic block') if in_atomic?
239
+ super
240
+ end
241
+
209
242
  module ClassMethods
210
243
  def persistable_value(k, v, options={})
211
244
  v.is_a?(PendingAtomic) ? v.compile_rql_value(options[:rql_doc]) : super
@@ -1,8 +1,9 @@
1
1
  module NoBrainer::Document::Attributes
2
2
  VALID_FIELD_OPTIONS = [:index, :default, :type, :readonly, :primary_key, :lazy_fetch, :store_as,
3
3
  :validates, :required, :unique, :uniq, :format, :in, :length, :min_length, :max_length]
4
- RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] \
5
- + NoBrainer::Criteria::Where::OPERATORS
4
+ RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] +
5
+ NoBrainer::SymbolDecoration::OPERATORS
6
+
6
7
  extend ActiveSupport::Concern
7
8
  include ActiveModel::ForbiddenAttributesProtection
8
9
 
@@ -48,8 +49,10 @@ module NoBrainer::Document::Attributes
48
49
 
49
50
  def assign_defaults(options)
50
51
  self.class.fields.each do |name, field_options|
51
- next unless field_options.has_key?(:default) &&
52
- !@_attributes.has_key?(name)
52
+ # :default => nil will not set the value to nil, but :default => ->{ nil } will.
53
+ # This is useful to unset a default value.
54
+
55
+ next if field_options[:default].nil? || @_attributes.key?(name)
53
56
 
54
57
  if opt = options[:missing_attributes]
55
58
  if (opt[:pluck] && !opt[:pluck][name]) ||
@@ -112,8 +115,14 @@ module NoBrainer::Document::Attributes
112
115
  super
113
116
  end
114
117
 
118
+ # The different between _field and field is that field can set other options
119
+ # (c.f. primary key module). _field always receive an immutable options list.
115
120
  def _field(attr, options={})
116
- # Using a layer so the user can use super when overriding these methods
121
+ options.assert_valid_keys(*VALID_FIELD_OPTIONS)
122
+ if attr.in?(RESERVED_FIELD_NAMES)
123
+ raise "The field name `:#{attr}' is reserved. Please use another one."
124
+ end
125
+
117
126
  attr = attr.to_s
118
127
  inject_in_layer :attributes do
119
128
  define_method("#{attr}=") { |value| _write_attribute(attr, value) }
@@ -124,14 +133,9 @@ module NoBrainer::Document::Attributes
124
133
  def field(attr, options={})
125
134
  attr = attr.to_sym
126
135
 
127
- options.assert_valid_keys(*VALID_FIELD_OPTIONS)
128
- if attr.in?(RESERVED_FIELD_NAMES)
129
- raise "Cannot use a reserved field attr: #{attr}"
130
- end
131
-
132
- ([self] + descendants).each do |model|
133
- model.fields[attr] ||= {}
134
- model.fields[attr].deep_merge!(options)
136
+ subclass_tree.each do |subclass|
137
+ subclass.fields[attr] ||= {}
138
+ subclass.fields[attr].deep_merge!(options)
135
139
  end
136
140
 
137
141
  _field(attr, self.fields[attr])
@@ -149,13 +153,18 @@ module NoBrainer::Document::Attributes
149
153
 
150
154
  _remove_field(attr, options)
151
155
 
152
- ([self] + descendants).each do |model|
153
- model.fields.delete(attr)
156
+ subclass_tree.each do |subclass|
157
+ subclass.fields.delete(attr)
154
158
  end
155
159
  end
156
160
 
157
161
  def has_field?(attr)
158
162
  !!fields[attr.to_sym]
159
163
  end
164
+
165
+ def ensure_valid_key!(key)
166
+ return if has_field?(key) || has_index?(key)
167
+ raise NoBrainer::Error::UnknownAttribute, "`#{key}' is not a valid attribute of #{self}"
168
+ end
160
169
  end
161
170
  end
@@ -20,7 +20,7 @@ module NoBrainer::Document::Callbacks
20
20
  run_callbacks(:update) { super }
21
21
  end
22
22
 
23
- def _save?(*args, &block)
23
+ def save?(*args, &block)
24
24
  run_callbacks(:save) { super }
25
25
  end
26
26
 
@@ -1,30 +1,30 @@
1
1
  module NoBrainer::Document::Core
2
2
  extend ActiveSupport::Concern
3
3
 
4
- singleton_class.class_eval do
5
- attr_accessor :_all, :_all_nobrainer
6
-
7
- def all
8
- Rails.application.eager_load! if defined?(Rails.application.eager_load!)
9
- @_all
10
- end
11
- end
4
+ singleton_class.class_eval { attr_accessor :_all }
12
5
  self._all = []
13
- self._all_nobrainer = []
14
-
15
- include ActiveModel::Conversion
16
-
17
- def to_key
18
- # ActiveModel::Conversion stuff
19
- [pk_value]
20
- end
21
6
 
22
7
  included do
23
8
  # TODO test these includes
24
9
  extend ActiveModel::Naming
25
10
  extend ActiveModel::Translation
26
11
 
27
- list_name = self.name =~ /^NoBrainer::/ ? :_all_nobrainer : :_all
28
- NoBrainer::Document::Core.__send__(list_name) << self
12
+ NoBrainer::Document::Core._all << self unless name =~ /^NoBrainer::/
13
+ end
14
+
15
+ def self.all(options={})
16
+ (options[:types] || [:user]).map do |type|
17
+ case type
18
+ when :user
19
+ Rails.application.eager_load! if defined?(Rails.application.eager_load!)
20
+ _all
21
+ when :nobrainer
22
+ [NoBrainer::Document::Index::MetaStore, NoBrainer::Lock]
23
+ when :system
24
+ NoBrainer::System.constants
25
+ .map { |c| NoBrainer::System.const_get(c) }
26
+ .select { |m| m < NoBrainer::Document }
27
+ end
28
+ end.reduce([], &:+)
29
29
  end
30
30
  end
@@ -17,8 +17,8 @@ module NoBrainer::Document::Criteria
17
17
  :raw, # Raw
18
18
  :limit, :offset, :skip, # Limit
19
19
  :order_by, :reverse_order, :without_ordering, :order_by_indexed?, :order_by_index_name, # OrderBy
20
- :scoped, :unscoped, # Scope
21
- :where, :where_indexed?, :where_index_name, :where_index_type, # Where
20
+ :unscoped, # Scope
21
+ :_where, :where, :where_indexed?, :where_index_name, :where_index_type, # Where
22
22
  :with_index, :without_index, :used_index, # Index
23
23
  :with_cache, :without_cache, # Cache
24
24
  :count, :empty?, :any?, # Count
@@ -26,14 +26,18 @@ module NoBrainer::Document::Criteria
26
26
  :preload, :eager_load, # EagerLoad
27
27
  :each, :to_a, # Enumerable
28
28
  :first, :last, :first!, :last!, :sample, # First
29
+ :upsert, :upsert!, :first_or_create, :first_or_create!, # FirstOrCreate
29
30
  :min, :max, :sum, :avg, # Aggregate
30
31
  :update_all, :replace_all, # Update
32
+ :changes, # Changes
31
33
  :pluck, :without, :lazy_fetch, :without_plucking, # Pluck
32
34
  :find_by?, :find_by, :find_by!, :find?, :find, :find!, # Find
35
+ :join, #Join
33
36
  :to => :all
34
37
 
35
38
  def all
36
- NoBrainer::Criteria.new(:model => self)
39
+ NoBrainer::Criteria.new(:initial_run_options => NoBrainer.current_run_options,
40
+ :model => self)
37
41
  end
38
42
 
39
43
  def scope(name, criteria=nil, &block)
@@ -48,8 +52,8 @@ module NoBrainer::Document::Criteria
48
52
  criteria_proc = block || (criteria.is_a?(Proc) ? criteria : proc { criteria })
49
53
  raise "default_scope only accepts a criteria or a proc that returns criteria" unless criteria_proc.is_a?(Proc)
50
54
 
51
- ([self] + self.descendants).each do |model|
52
- model.default_scopes << criteria_proc
55
+ subclass_tree.each do |subclass|
56
+ subclass.default_scopes << criteria_proc
53
57
  end
54
58
  end
55
59
 
@@ -1,10 +1,11 @@
1
1
  module NoBrainer::Document::Dirty
2
2
  extend ActiveSupport::Concern
3
- # We need to save the changes as seen through read_attribute because
4
- # the user sees attributes through the read_attribute getters.
5
- # But we want to detect changes based on @_attributes to track
6
- # things like undefined -> nil. Going through the getters will
7
- # not give us that.
3
+ # 1) We should save the changes as seen through read_attribute, because the
4
+ # user sees attributes through the read_attribute getters, but it's near
5
+ # impossible because we would need to wrap the user defined getters, so we'll
6
+ # go through _read_attribute.
7
+ # 2) We want to detect changes based on @_attributes to track things like
8
+ # undefined -> nil. Going through the getters will not give us that.
8
9
 
9
10
  def _create(*args)
10
11
  super.tap { clear_dirtiness }
@@ -17,7 +18,9 @@ module NoBrainer::Document::Dirty
17
18
  def clear_dirtiness(options={})
18
19
  if options[:keep_ivars] && options[:missing_attributes].try(:[], :pluck)
19
20
  attrs = options[:missing_attributes][:pluck].keys
20
- @_old_attributes = @_old_attributes.reject { |k,v| attrs.include?(k) } else @_old_attributes = {}.with_indifferent_access
21
+ @_old_attributes = @_old_attributes.reject { |k,v| attrs.include?(k) }
22
+ else
23
+ @_old_attributes = {}.with_indifferent_access
21
24
  end
22
25
 
23
26
  @_old_attributes_keys = @_attributes.keys # to track undefined -> nil changes
@@ -34,7 +37,7 @@ module NoBrainer::Document::Dirty
34
37
  def changes
35
38
  result = {}.with_indifferent_access
36
39
  @_old_attributes.each do |attr, old_value|
37
- current_value = read_attribute(attr)
40
+ current_value = _read_attribute(attr)
38
41
  if current_value != old_value || !@_old_attributes_keys.include?(attr)
39
42
  result[attr] = [old_value, current_value]
40
43
  end
@@ -47,13 +50,13 @@ module NoBrainer::Document::Dirty
47
50
  if current_value == None
48
51
  current_value = begin
49
52
  assert_access_field(attr)
50
- read_attribute(attr)
53
+ _read_attribute(attr)
51
54
  rescue NoBrainer::Error::MissingAttribute => e
52
55
  e
53
56
  end
54
57
  end
55
58
 
56
- unless @_old_attributes.has_key?(attr)
59
+ unless @_old_attributes.key?(attr)
57
60
  @_old_attributes[attr] = current_value.deep_dup
58
61
  end
59
62
  end
@@ -78,8 +81,8 @@ module NoBrainer::Document::Dirty
78
81
 
79
82
  inject_in_layer :dirty_tracking do
80
83
  define_method("#{attr}_change") do
81
- if @_old_attributes.has_key?(attr)
82
- result = [@_old_attributes[attr], read_attribute(attr)]
84
+ if @_old_attributes.key?(attr)
85
+ result = [@_old_attributes[attr], _read_attribute(attr)]
83
86
  result if result.first != result.last || !@_old_attributes_keys.include?(attr)
84
87
  end
85
88
  end
@@ -89,7 +92,7 @@ module NoBrainer::Document::Dirty
89
92
  end
90
93
 
91
94
  define_method("#{attr}_was") do
92
- @_old_attributes.has_key?(attr) ? @_old_attributes[attr] : read_attribute(attr)
95
+ @_old_attributes.key?(attr) ? @_old_attributes[attr] : _read_attribute(attr)
93
96
  end
94
97
  end
95
98
  end
@@ -12,4 +12,10 @@ module NoBrainer::Document::DynamicAttributes
12
12
  def readable_attributes
13
13
  @_attributes.keys
14
14
  end
15
+
16
+ module ClassMethods
17
+ def ensure_valid_key!(key)
18
+ # we never raise
19
+ end
20
+ end
15
21
  end