nobrainer 0.18.0 → 0.22.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +0 -5
  3. data/lib/no_brainer/config.rb +73 -39
  4. data/lib/no_brainer/connection.rb +2 -4
  5. data/lib/no_brainer/criteria/after_find.rb +3 -11
  6. data/lib/no_brainer/criteria/aggregate.rb +2 -2
  7. data/lib/no_brainer/criteria/cache.rb +15 -10
  8. data/lib/no_brainer/criteria/core.rb +46 -11
  9. data/lib/no_brainer/criteria/delete.rb +2 -2
  10. data/lib/no_brainer/criteria/eager_load.rb +51 -0
  11. data/lib/no_brainer/criteria/extend.rb +4 -16
  12. data/lib/no_brainer/criteria/find.rb +27 -0
  13. data/lib/no_brainer/criteria/index.rb +7 -13
  14. data/lib/no_brainer/criteria/limit.rb +5 -12
  15. data/lib/no_brainer/criteria/order_by.rb +20 -36
  16. data/lib/no_brainer/criteria/pluck.rb +16 -22
  17. data/lib/no_brainer/criteria/raw.rb +4 -10
  18. data/lib/no_brainer/criteria/scope.rb +6 -19
  19. data/lib/no_brainer/criteria/update.rb +8 -6
  20. data/lib/no_brainer/criteria/where.rb +252 -138
  21. data/lib/no_brainer/criteria.rb +3 -2
  22. data/lib/no_brainer/document/aliases.rb +3 -3
  23. data/lib/no_brainer/document/association/belongs_to.rb +9 -5
  24. data/lib/no_brainer/document/association/core.rb +6 -5
  25. data/lib/no_brainer/document/association/eager_loader.rb +9 -9
  26. data/lib/no_brainer/document/association/has_many.rb +23 -9
  27. data/lib/no_brainer/document/association/has_many_through.rb +12 -3
  28. data/lib/no_brainer/document/atomic_ops.rb +79 -78
  29. data/lib/no_brainer/document/attributes.rb +24 -20
  30. data/lib/no_brainer/document/callbacks.rb +1 -1
  31. data/lib/no_brainer/document/core.rb +5 -2
  32. data/lib/no_brainer/document/criteria.rb +14 -19
  33. data/lib/no_brainer/document/dirty.rb +11 -16
  34. data/lib/no_brainer/document/index/index.rb +2 -1
  35. data/lib/no_brainer/document/index/meta_store.rb +1 -1
  36. data/lib/no_brainer/document/index.rb +14 -10
  37. data/lib/no_brainer/document/persistance.rb +24 -13
  38. data/lib/no_brainer/document/primary_key/generator.rb +83 -0
  39. data/lib/no_brainer/document/{id.rb → primary_key.rb} +9 -36
  40. data/lib/no_brainer/document/store_in.rb +2 -2
  41. data/lib/no_brainer/document/timestamps.rb +4 -2
  42. data/lib/no_brainer/document/types/binary.rb +2 -7
  43. data/lib/no_brainer/document/types/boolean.rb +2 -4
  44. data/lib/no_brainer/document/types/date.rb +2 -2
  45. data/lib/no_brainer/document/types/float.rb +2 -2
  46. data/lib/no_brainer/document/types/geo.rb +1 -0
  47. data/lib/no_brainer/document/types/integer.rb +2 -2
  48. data/lib/no_brainer/document/types/set.rb +2 -2
  49. data/lib/no_brainer/document/types/string.rb +5 -2
  50. data/lib/no_brainer/document/types/symbol.rb +2 -2
  51. data/lib/no_brainer/document/types/text.rb +18 -0
  52. data/lib/no_brainer/document/types/time.rb +2 -2
  53. data/lib/no_brainer/document/types.rb +17 -18
  54. data/lib/no_brainer/document/validation/not_null.rb +15 -0
  55. data/lib/no_brainer/document/{uniqueness.rb → validation/uniqueness.rb} +11 -11
  56. data/lib/no_brainer/document/validation.rb +35 -6
  57. data/lib/no_brainer/document.rb +1 -1
  58. data/lib/no_brainer/error.rb +21 -19
  59. data/lib/no_brainer/geo/base.rb +16 -0
  60. data/lib/no_brainer/geo/circle.rb +25 -0
  61. data/lib/no_brainer/geo/line_string.rb +11 -0
  62. data/lib/no_brainer/geo/point.rb +49 -0
  63. data/lib/no_brainer/geo/polygon.rb +11 -0
  64. data/lib/no_brainer/geo.rb +4 -0
  65. data/lib/no_brainer/locale/en.yml +1 -0
  66. data/lib/no_brainer/lock.rb +114 -0
  67. data/lib/no_brainer/query_runner/connection_lock.rb +1 -1
  68. data/lib/no_brainer/query_runner/database_on_demand.rb +0 -1
  69. data/lib/no_brainer/query_runner/missing_index.rb +1 -1
  70. data/lib/no_brainer/query_runner/reconnect.rb +9 -11
  71. data/lib/no_brainer/query_runner/run_options.rb +0 -3
  72. data/lib/no_brainer/query_runner/table_on_demand.rb +3 -4
  73. data/lib/no_brainer/railtie/database.rake +2 -2
  74. data/lib/no_brainer/rql.rb +1 -5
  75. data/lib/nobrainer.rb +2 -6
  76. data/lib/rails/generators/nobrainer.rb +1 -1
  77. metadata +34 -9
  78. data/lib/no_brainer/criteria/preload.rb +0 -50
  79. data/lib/no_brainer/decorated_symbol.rb +0 -17
@@ -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
 
@@ -17,7 +16,7 @@ class NoBrainer::Document::Association::EagerLoader
17
16
  if owner_keys.present?
18
17
  targets = criteria.where(target_key.in => owner_keys)
19
18
  .map { |target| [target.read_attribute(target_key), target] }
20
- .reduce(Hash.new { |k,v| k[v] = [] }) { |h,(k,v)| h[k] << v; h }
19
+ .each_with_object(Hash.new { |k,v| k[v] = [] }) { |(k,v),h| h[k] << v }
21
20
 
22
21
  unloaded_docs.each do |doc|
23
22
  doc_targets = targets[doc.read_attribute(owner_key)]
@@ -40,19 +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._preloads = v._preloads, []
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) }
55
- else eager_load_association(docs, preloads)
53
+ when Array then what.each { |v| eager_load(docs, v) }
54
+ when nil then;
55
+ else eager_load_association(docs, what) # String and Symbol
56
56
  end
57
57
  true
58
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?
@@ -80,18 +96,16 @@ class NoBrainer::Document::Association::HasMany
80
96
  end
81
97
 
82
98
  def set_inverse_proc
83
- lambda { |target| set_inverses_of([target]) if target.is_a?(NoBrainer::Document) }
99
+ ->(target){ set_inverses_of([target]) if target.is_a?(NoBrainer::Document) }
84
100
  end
85
101
 
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,103 +2,125 @@ module NoBrainer::Document::AtomicOps
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  class PendingAtomic
5
- def self._new(instance, field, value, is_user_value, options={})
6
- model = case value
7
- when Array then PendingAtomicArray
8
- when Set then PendingAtomicSet
9
- else self
10
- end
11
- model.new(instance, field, value, is_user_value, options)
5
+ attr_accessor :type
6
+
7
+ def self._new(instance, field, value, is_user_value)
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
14
+ else self
15
+ end.new(instance, field, value, is_user_value, type)
12
16
  end
13
17
 
14
- def initialize(instance, field, value, is_user_value, options={})
18
+ def initialize(instance, field, value, is_user_value, type)
15
19
  @instance = instance
16
20
  @field = field.to_s
17
21
  @value = value
18
22
  @is_user_value = is_user_value
19
- @options = options.dup
23
+ @type = type
20
24
  @ops = []
21
25
  end
22
26
 
23
- def write_access?
24
- !!@options[:write_access]
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
25
35
  end
26
36
 
27
- def ensure_writeable!
28
- unless write_access?
29
- @options[:write_access] = true
30
- @instance.write_attribute(@field, self)
31
- end
37
+ def initialize_copy(other)
38
+ super
39
+ @ops = @ops.dup
32
40
  end
33
41
 
34
42
  def to_s
35
- "<#{@field} with pending atomic operations>"
43
+ "<`#{@field}' with #{@ops.size} pending atomic operations>"
36
44
  end
37
- alias inspect to_s
45
+ alias_method :inspect, :to_s
46
+
47
+ def method_missing(method, *a, &b)
48
+ if method == :<<
49
+ method = :append
50
+ modify_source!
51
+ end
38
52
 
39
- def method_missing(method_name, *a, &b)
40
- @ops << [method_name, a, b]
53
+ @ops << [method, a, b]
41
54
  self
42
55
  end
43
56
 
44
57
  def compile_rql_value(rql_doc)
45
58
  field = @instance.class.lookup_field_alias(@field)
46
59
  value = @is_user_value ? RethinkDB::RQL.new.expr(@value) : rql_doc[field]
47
- @ops.each { |method_name, a, b| value = value.__send__(method_name, *a, &b) }
48
- value
60
+ value = value.default(default_value) if default_value
61
+ @ops.reduce(value) { |v, (method, a, b)| v.__send__(method, *a, &b) }
62
+ end
63
+
64
+ def modify_source!
65
+ unless @instance._is_attribute_touched?(@field)
66
+ @instance.write_attribute(@field, self)
67
+ end
49
68
  end
50
69
  end
51
70
 
52
- class PendingAtomicArray < PendingAtomic
53
- def -(value)
54
- @ops << [:difference, [value.to_a]]
71
+ class PendingAtomicContainer < PendingAtomic
72
+ def &(value)
73
+ @ops << [:set_intersection, [value.to_a]]
55
74
  self
56
75
  end
57
- def difference(v); self - v; end
58
76
 
59
- def delete(value)
60
- difference([value])
77
+ def |(value)
78
+ @ops << [:set_union, [value.to_a]]
79
+ self
61
80
  end
62
81
 
63
- def +(value)
64
- @ops << [:+, [value.to_a]]
65
- self
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
86
+
87
+ def delete(value)
88
+ difference([value])
66
89
  end
67
- def add(v); self + v; end
90
+ end
68
91
 
69
- def &(value)
70
- @ops << [:set_intersection, [value.to_a]]
92
+ class PendingAtomicSet < PendingAtomicContainer
93
+ def <<(value)
94
+ @ops << [:set_union, [[value]]]
95
+ modify_source!
71
96
  self
72
97
  end
73
- def intersection(v); self & v; end
74
98
 
75
- def |(value)
99
+ def +(value)
76
100
  @ops << [:set_union, [value.to_a]]
77
101
  self
78
102
  end
79
- def union(v); self | v; end
80
103
 
81
- def <<(value)
82
- @ops << [:append, [value]]
83
- ensure_writeable!
104
+ def -(value)
105
+ @ops << [:set_difference, [value.to_a]]
84
106
  self
85
107
  end
86
108
  end
87
109
 
88
- class PendingAtomicSet < PendingAtomicArray
89
- def -(value)
90
- @ops << [:set_difference, [value.to_a]]
110
+ class PendingAtomicArray < PendingAtomicContainer
111
+ def <<(value)
112
+ @ops << [:append, [value]]
113
+ modify_source!
91
114
  self
92
115
  end
93
116
 
94
117
  def +(value)
95
- @ops << [:set_union, [value.to_a]]
118
+ @ops << [:+, [value.to_a]]
96
119
  self
97
120
  end
98
121
 
99
- def <<(value)
100
- @ops << [:set_union, [[value]]]
101
- ensure_writeable!
122
+ def -(value)
123
+ @ops << [:difference, [value.to_a]]
102
124
  self
103
125
  end
104
126
  end
@@ -109,6 +131,9 @@ module NoBrainer::Document::AtomicOps
109
131
  end
110
132
 
111
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.
112
137
  @_touched_attributes << name.to_s
113
138
  end
114
139
 
@@ -143,12 +168,12 @@ module NoBrainer::Document::AtomicOps
143
168
  def _read_attribute(name)
144
169
  ensure_exclusive_atomic!
145
170
  value = super
171
+ return value unless in_atomic?
146
172
 
147
- case [in_atomic?, value.is_a?(PendingAtomic)]
148
- when [true, true] then value
149
- when [true, false] then PendingAtomic._new(self, name, value, _is_attribute_touched?(name))
150
- when [false, true] then raise NoBrainer::Error::CannotReadAtomic.new(self, name, value)
151
- 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))
152
177
  end
153
178
  end
154
179
 
@@ -156,9 +181,9 @@ module NoBrainer::Document::AtomicOps
156
181
  ensure_exclusive_atomic!
157
182
 
158
183
  case [in_atomic?, value.is_a?(PendingAtomic)]
159
- when [true, true] then super
160
184
  when [true, false] then raise NoBrainer::Error::AtomicBlock.new('Avoid the use of atomic blocks for non atomic operations')
161
185
  when [false, true] then raise NoBrainer::Error::AtomicBlock.new('Use atomic blocks for atomic operations')
186
+ when [true, true] then super.tap { _touch_attribute(name) }
162
187
  when [false, false] then super.tap { _touch_attribute(name) }
163
188
  end
164
189
  end
@@ -175,39 +200,15 @@ module NoBrainer::Document::AtomicOps
175
200
  if saved
176
201
  @_attributes.each do |attr, value|
177
202
  next unless value.is_a?(PendingAtomic)
178
- @_attributes[attr] = value.class.new(self, attr, nil, false)
203
+ @_attributes[attr] = value.class.new(self, attr, nil, false, value.type)
179
204
  end
180
205
  end
181
206
  end
182
207
  end
183
208
 
184
- def read_attribute_for_change(attr)
185
- super
186
- rescue NoBrainer::Error::CannotReadAtomic => e
187
- e.value
188
- end
189
-
190
- def read_attribute_for_validation(attr)
191
- super
192
- rescue NoBrainer::Error::CannotReadAtomic => e
193
- e.value
194
- end
195
-
196
209
  module ClassMethods
197
210
  def persistable_value(k, v, options={})
198
211
  v.is_a?(PendingAtomic) ? v.compile_rql_value(options[:rql_doc]) : super
199
212
  end
200
213
  end
201
214
  end
202
-
203
- class ActiveModel::EachValidator
204
- # XXX Monkey Patching :(
205
- def validate(record)
206
- attributes.each do |attribute|
207
- value = record.read_attribute_for_validation(attribute)
208
- next if value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic) # <--- This is the added line
209
- next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
210
- validate_each(record, attribute, value)
211
- end
212
- end
213
- end
@@ -1,14 +1,12 @@
1
1
  module NoBrainer::Document::Attributes
2
- VALID_FIELD_OPTIONS = [:index, :default, :type, :real_type,
3
- :validates, :required, :unique, :uniq, :format, :in,
4
- :readonly, :primary_key, :as, :lazy_fetch]
2
+ VALID_FIELD_OPTIONS = [:index, :default, :type, :readonly, :primary_key, :lazy_fetch, :store_as,
3
+ :validates, :required, :unique, :uniq, :format, :in, :length, :min_length, :max_length]
5
4
  RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] \
6
- + NoBrainer::DecoratedSymbol::MODIFIERS.keys
5
+ + NoBrainer::Criteria::Where::OPERATORS
7
6
  extend ActiveSupport::Concern
7
+ include ActiveModel::ForbiddenAttributesProtection
8
8
 
9
9
  included do
10
- # Not using class_attribute because we want to
11
- # use our custom logic
12
10
  singleton_class.send(:attr_accessor, :fields)
13
11
  self.fields = {}
14
12
  end
@@ -26,6 +24,10 @@ module NoBrainer::Document::Attributes
26
24
  Hash[readable_attributes.map { |k| [k, read_attribute(k)] }].with_indifferent_access.freeze
27
25
  end
28
26
 
27
+ def raw_attributes
28
+ @_attributes
29
+ end
30
+
29
31
  def _read_attribute(name)
30
32
  @_attributes[name]
31
33
  end
@@ -46,24 +48,25 @@ module NoBrainer::Document::Attributes
46
48
 
47
49
  def assign_defaults(options)
48
50
  self.class.fields.each do |name, field_options|
49
- if field_options.has_key?(:default) &&
50
- !@_attributes.has_key?(name)
51
-
52
- if opt = options[:missing_attributes]
53
- if (opt[:pluck] && !opt[:pluck][name]) ||
54
- (opt[:without] && opt[:without][name])
55
- next
56
- end
57
- end
58
-
59
- default_value = field_options[:default]
60
- default_value = default_value.call if default_value.is_a?(Proc)
61
- self.write_attribute(name, default_value)
51
+ next unless field_options.has_key?(:default) &&
52
+ !@_attributes.has_key?(name)
53
+
54
+ if opt = options[:missing_attributes]
55
+ if (opt[:pluck] && !opt[:pluck][name]) ||
56
+ (opt[:without] && opt[:without][name])
57
+ next
58
+ end
62
59
  end
60
+
61
+ default_value = field_options[:default]
62
+ default_value = instance_exec(&default_value) if default_value.is_a?(Proc)
63
+ self.write_attribute(name, default_value)
63
64
  end
64
65
  end
65
66
 
66
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
+
67
70
  if options[:pristine]
68
71
  if options[:keep_ivars] && options[:missing_attributes].try(:[], :pluck)
69
72
  options[:missing_attributes][:pluck].keys.each { |k| @_attributes.delete(k) }
@@ -78,6 +81,7 @@ module NoBrainer::Document::Attributes
78
81
  clear_dirtiness(options)
79
82
  else
80
83
  clear_dirtiness(options) if options[:pristine]
84
+ attrs = sanitize_for_mass_assignment(attrs)
81
85
  attrs.each { |k,v| self.write_attribute(k,v) }
82
86
  end
83
87
  assign_defaults(options) if options[:pristine]
@@ -86,7 +90,7 @@ module NoBrainer::Document::Attributes
86
90
 
87
91
  def inspectable_attributes
88
92
  # TODO test that thing
89
- 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
90
94
  end
91
95
 
92
96
  def to_s
@@ -9,7 +9,7 @@ module NoBrainer::Document::Callbacks
9
9
  end
10
10
 
11
11
  def initialize(*args, &block)
12
- run_callbacks(:initialize) { _initialize(*args); true }
12
+ run_callbacks(:initialize) { _initialize(*args, &block); true }
13
13
  end
14
14
 
15
15
  def _create(*args, &block)
@@ -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
@@ -2,12 +2,14 @@ module NoBrainer::Document::Criteria
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def selector
5
- self.class.selector_for(pk_value)
5
+ # Used for writes
6
+ self.class.rql_table.get(pk_value)
6
7
  end
7
8
 
8
9
  included do
9
- cattr_accessor :default_scope_proc, :instance_accessor => false
10
10
  cattr_accessor :perf_warnings_disabled, :instance_accessor => false
11
+ singleton_class.send(:attr_accessor, :default_scopes)
12
+ self.default_scopes = []
11
13
  end
12
14
 
13
15
  module ClassMethods
@@ -21,12 +23,13 @@ module NoBrainer::Document::Criteria
21
23
  :with_cache, :without_cache, # Cache
22
24
  :count, :empty?, :any?, # Count
23
25
  :delete_all, :destroy_all, # Delete
24
- :includes, :preload, # Preload
26
+ :preload, :eager_load, # EagerLoad
25
27
  :each, :to_a, # Enumerable
26
28
  :first, :last, :first!, :last!, :sample, # First
27
29
  :min, :max, :sum, :avg, # Aggregate
28
30
  :update_all, :replace_all, # Update
29
31
  :pluck, :without, :lazy_fetch, :without_plucking, # Pluck
32
+ :find_by?, :find_by, :find_by!, :find?, :find, :find!, # Find
30
33
  :to => :all
31
34
 
32
35
  def all
@@ -42,25 +45,17 @@ module NoBrainer::Document::Criteria
42
45
  end
43
46
 
44
47
  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 }
48
- end
49
-
50
- def selector_for(pk)
51
- rql_table.get(pk)
52
- end
48
+ criteria_proc = block || (criteria.is_a?(Proc) ? criteria : proc { criteria })
49
+ raise "default_scope only accepts a criteria or a proc that returns criteria" unless criteria_proc.is_a?(Proc)
53
50
 
54
- # XXX this doesn't have the same semantics as other ORMs. the equivalent is find!.
55
- def find(pk)
56
- attrs = NoBrainer.run { selector_for(pk) }
57
- new_from_db(attrs).tap { |doc| doc.run_callbacks(:find) } if attrs
51
+ ([self] + self.descendants).each do |model|
52
+ model.default_scopes << criteria_proc
53
+ end
58
54
  end
59
55
 
60
- def find!(pk)
61
- find(pk).tap do |doc|
62
- raise NoBrainer::Error::DocumentNotFound, "#{self} #{pk_name}: #{pk} not found" unless doc
63
- end
56
+ def inherited(subclass)
57
+ subclass.default_scopes = self.default_scopes.dup
58
+ super
64
59
  end
65
60
 
66
61
  def disable_perf_warnings
@@ -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
@@ -9,7 +9,8 @@ class NoBrainer::Document::Index::Index < Struct.new(
9
9
  self.name = self.name.to_sym
10
10
  self.aliased_name = self.aliased_name.to_sym
11
11
  self.external = !!self.external
12
- self.geo = !!self.geo
12
+ # geo defaults for true with geo types.
13
+ self.geo = !!model.fields[name].try(:[], :type).try(:<, NoBrainer::Geo::Base) if self.geo.nil?
13
14
  self.multi = !!self.multi
14
15
  end
15
16