nobrainer 0.16.0 → 0.18.1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +53 -28
  3. data/lib/no_brainer/connection.rb +1 -0
  4. data/lib/no_brainer/connection_manager.rb +60 -0
  5. data/lib/no_brainer/criteria/after_find.rb +3 -11
  6. data/lib/no_brainer/criteria/aggregate.rb +8 -5
  7. data/lib/no_brainer/criteria/cache.rb +7 -7
  8. data/lib/no_brainer/criteria/core.rb +56 -16
  9. data/lib/no_brainer/criteria/count.rb +1 -1
  10. data/lib/no_brainer/criteria/delete.rb +1 -1
  11. data/lib/no_brainer/criteria/extend.rb +19 -0
  12. data/lib/no_brainer/criteria/first.rb +1 -1
  13. data/lib/no_brainer/criteria/index.rb +7 -13
  14. data/lib/no_brainer/criteria/limit.rb +5 -13
  15. data/lib/no_brainer/criteria/order_by.rb +22 -41
  16. data/lib/no_brainer/criteria/pluck.rb +17 -23
  17. data/lib/no_brainer/criteria/preload.rb +9 -15
  18. data/lib/no_brainer/criteria/raw.rb +5 -11
  19. data/lib/no_brainer/criteria/scope.rb +9 -15
  20. data/lib/no_brainer/criteria/update.rb +3 -3
  21. data/lib/no_brainer/criteria/where.rb +33 -56
  22. data/lib/no_brainer/criteria.rb +1 -1
  23. data/lib/no_brainer/document/aliases.rb +1 -1
  24. data/lib/no_brainer/document/association/belongs_to.rb +6 -6
  25. data/lib/no_brainer/document/association/core.rb +11 -11
  26. data/lib/no_brainer/document/association/eager_loader.rb +4 -3
  27. data/lib/no_brainer/document/association/has_many.rb +7 -7
  28. data/lib/no_brainer/document/association/has_many_through.rb +1 -1
  29. data/lib/no_brainer/document/association/has_one.rb +1 -1
  30. data/lib/no_brainer/document/association.rb +5 -5
  31. data/lib/no_brainer/document/atomic_ops.rb +213 -0
  32. data/lib/no_brainer/document/attributes.rb +19 -10
  33. data/lib/no_brainer/document/callbacks.rb +1 -1
  34. data/lib/no_brainer/document/core.rb +1 -1
  35. data/lib/no_brainer/document/criteria.rb +9 -2
  36. data/lib/no_brainer/document/dirty.rb +22 -22
  37. data/lib/no_brainer/document/dynamic_attributes.rb +2 -14
  38. data/lib/no_brainer/document/id.rb +1 -0
  39. data/lib/no_brainer/document/index/index.rb +83 -0
  40. data/lib/no_brainer/document/index/meta_store.rb +31 -0
  41. data/lib/no_brainer/document/index/synchronizer.rb +68 -0
  42. data/lib/no_brainer/document/index.rb +13 -72
  43. data/lib/no_brainer/document/lazy_fetch.rb +5 -5
  44. data/lib/no_brainer/document/missing_attributes.rb +6 -23
  45. data/lib/no_brainer/document/persistance.rb +38 -21
  46. data/lib/no_brainer/document/polymorphic.rb +1 -1
  47. data/lib/no_brainer/document/types/set.rb +23 -0
  48. data/lib/no_brainer/document/types.rb +8 -6
  49. data/lib/no_brainer/document/uniqueness.rb +3 -3
  50. data/lib/no_brainer/document/validation.rb +13 -4
  51. data/lib/no_brainer/document.rb +1 -1
  52. data/lib/no_brainer/error.rb +14 -0
  53. data/lib/no_brainer/fork.rb +1 -0
  54. data/lib/no_brainer/query_runner/connection_lock.rb +5 -1
  55. data/lib/no_brainer/query_runner/database_on_demand.rb +6 -5
  56. data/lib/no_brainer/query_runner/logger.rb +10 -6
  57. data/lib/no_brainer/query_runner/missing_index.rb +5 -4
  58. data/lib/no_brainer/query_runner/reconnect.rb +20 -17
  59. data/lib/no_brainer/query_runner/run_options.rb +3 -0
  60. data/lib/no_brainer/query_runner/table_on_demand.rb +11 -8
  61. data/lib/no_brainer/railtie/database.rake +12 -12
  62. data/lib/no_brainer/railtie.rb +5 -5
  63. data/lib/no_brainer/rql.rb +9 -0
  64. data/lib/nobrainer.rb +9 -23
  65. metadata +10 -4
  66. data/lib/no_brainer/index_manager.rb +0 -9
@@ -7,7 +7,7 @@ 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_klass.all
10
+ criteria = target_model.all
11
11
  criteria = criteria.merge(additional_criteria) if additional_criteria
12
12
  criteria = criteria.unscoped if options[:unscoped]
13
13
 
@@ -45,14 +45,15 @@ class NoBrainer::Document::Association::EagerLoader
45
45
  when Hash then preloads.each do |k,v|
46
46
  if v.is_a?(NoBrainer::Criteria)
47
47
  v = v.dup
48
- nested_preloads, v._preloads = v._preloads, []
48
+ nested_preloads, v.options[:preload] = v.options[:preload], []
49
49
  eager_load(eager_load_association(docs, k, v), nested_preloads)
50
50
  else
51
51
  eager_load(eager_load_association(docs, k), v)
52
52
  end
53
53
  end
54
54
  when Array then preloads.each { |v| eager_load(docs, v) }
55
- else eager_load_association(docs, preloads)
55
+ when nil then;
56
+ else eager_load_association(docs, preloads) # String and Symbol
56
57
  end
57
58
  true
58
59
  end
@@ -8,15 +8,15 @@ class NoBrainer::Document::Association::HasMany
8
8
 
9
9
  def foreign_key
10
10
  # TODO test :foreign_key
11
- options[:foreign_key].try(:to_sym) || :"#{owner_klass.name.underscore}_#{owner_klass.pk_name}"
11
+ options[:foreign_key].try(:to_sym) || :"#{owner_model.name.underscore}_#{owner_model.pk_name}"
12
12
  end
13
13
 
14
14
  def primary_key
15
15
  # TODO test :primary_key
16
- options[:primary_key].try(:to_sym) || target_klass.pk_name
16
+ options[:primary_key].try(:to_sym) || target_model.pk_name
17
17
  end
18
18
 
19
- def target_klass
19
+ def target_model
20
20
  # TODO test :class_name
21
21
  (options[:class_name] || target_name.to_s.singularize.camelize).constantize
22
22
  end
@@ -27,11 +27,11 @@ class NoBrainer::Document::Association::HasMany
27
27
  # selector.
28
28
  # XXX Without caching, this is going to get CPU intensive quickly, but
29
29
  # caching is hard (rails console reload, etc.).
30
- target_klass.association_metadata.values.select do |assoc|
30
+ target_model.association_metadata.values.select do |assoc|
31
31
  assoc.is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) and
32
32
  assoc.foreign_key == self.foreign_key and
33
33
  assoc.primary_key == self.primary_key and
34
- assoc.target_klass.root_class == owner_klass.root_class
34
+ assoc.target_model.root_class == owner_model.root_class
35
35
  end
36
36
  end
37
37
 
@@ -44,7 +44,7 @@ class NoBrainer::Document::Association::HasMany
44
44
  end
45
45
 
46
46
  def target_criteria
47
- @target_criteria ||= target_klass.where(foreign_key => owner.pk_value)
47
+ @target_criteria ||= target_model.where(foreign_key => owner.pk_value)
48
48
  .after_find(set_inverse_proc)
49
49
  end
50
50
 
@@ -54,7 +54,7 @@ class NoBrainer::Document::Association::HasMany
54
54
 
55
55
  def write(new_children)
56
56
  raise "You can't assign #{target_name}. " +
57
- "Instead, you must modify delete and create #{target_klass} manually."
57
+ "Instead, you must modify delete and create #{target_model} manually."
58
58
  end
59
59
 
60
60
  def loaded?
@@ -10,7 +10,7 @@ class NoBrainer::Document::Association::HasManyThrough
10
10
  end
11
11
 
12
12
  def through_association
13
- owner_klass.association_metadata[through_association_name] or
13
+ owner_model.association_metadata[through_association_name] or
14
14
  raise "#{through_association_name} association not found"
15
15
  end
16
16
 
@@ -1,6 +1,6 @@
1
1
  class NoBrainer::Document::Association::HasOne < NoBrainer::Document::Association::HasMany
2
2
  class Metadata < NoBrainer::Document::Association::HasMany::Metadata
3
- def target_klass
3
+ def target_model
4
4
  (options[:class_name] || target_name.to_s.camelize).constantize
5
5
  end
6
6
  end
@@ -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
- klass_name = (options[:through] ? "#{association}_through" : association.to_s).camelize
32
- metadata_klass = NoBrainer::Document::Association.const_get(klass_name).const_get(:Metadata)
33
- r = metadata_klass.new(self, target, options)
34
- ([self] + descendants).each do |klass|
35
- klass.association_metadata[target] = r
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
36
36
  end
37
37
  end
38
38
  r.hook
@@ -0,0 +1,213 @@
1
+ module NoBrainer::Document::AtomicOps
2
+ extend ActiveSupport::Concern
3
+
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)
12
+ end
13
+
14
+ def initialize(instance, field, value, is_user_value, options={})
15
+ @instance = instance
16
+ @field = field.to_s
17
+ @value = value
18
+ @is_user_value = is_user_value
19
+ @options = options.dup
20
+ @ops = []
21
+ end
22
+
23
+ def write_access?
24
+ !!@options[:write_access]
25
+ end
26
+
27
+ def ensure_writeable!
28
+ unless write_access?
29
+ @options[:write_access] = true
30
+ @instance.write_attribute(@field, self)
31
+ end
32
+ end
33
+
34
+ def to_s
35
+ "<#{@field} with pending atomic operations>"
36
+ end
37
+ alias inspect to_s
38
+
39
+ def method_missing(method_name, *a, &b)
40
+ @ops << [method_name, a, b]
41
+ self
42
+ end
43
+
44
+ def compile_rql_value(rql_doc)
45
+ field = @instance.class.lookup_field_alias(@field)
46
+ 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
49
+ end
50
+ end
51
+
52
+ class PendingAtomicArray < PendingAtomic
53
+ def -(value)
54
+ @ops << [:difference, [value.to_a]]
55
+ self
56
+ end
57
+ def difference(v); self - v; end
58
+
59
+ def delete(value)
60
+ difference([value])
61
+ end
62
+
63
+ def +(value)
64
+ @ops << [:+, [value.to_a]]
65
+ self
66
+ end
67
+ def add(v); self + v; end
68
+
69
+ def &(value)
70
+ @ops << [:set_intersection, [value.to_a]]
71
+ self
72
+ end
73
+ def intersection(v); self & v; end
74
+
75
+ def |(value)
76
+ @ops << [:set_union, [value.to_a]]
77
+ self
78
+ end
79
+ def union(v); self | v; end
80
+
81
+ def <<(value)
82
+ @ops << [:append, [value]]
83
+ ensure_writeable!
84
+ self
85
+ end
86
+ end
87
+
88
+ class PendingAtomicSet < PendingAtomicArray
89
+ def -(value)
90
+ @ops << [:set_difference, [value.to_a]]
91
+ self
92
+ end
93
+
94
+ def +(value)
95
+ @ops << [:set_union, [value.to_a]]
96
+ self
97
+ end
98
+
99
+ def <<(value)
100
+ @ops << [:set_union, [[value]]]
101
+ ensure_writeable!
102
+ self
103
+ end
104
+ end
105
+
106
+ def clear_dirtiness(options={})
107
+ super
108
+ @_touched_attributes = Set.new
109
+ end
110
+
111
+ def _touch_attribute(name)
112
+ @_touched_attributes << name.to_s
113
+ end
114
+
115
+ def _is_attribute_touched?(name)
116
+ @_touched_attributes.include?(name.to_s)
117
+ end
118
+
119
+ def in_atomic?
120
+ !!Thread.current[:nobrainer_atomic]
121
+ end
122
+
123
+ def in_other_atomic?
124
+ v = Thread.current[:nobrainer_atomic]
125
+ !v.nil? && !v.equal?(self)
126
+ end
127
+
128
+ def ensure_exclusive_atomic!
129
+ raise NoBrainer::Error::AtomicBlock.new('You may not access other documents within an atomic block') if in_other_atomic?
130
+ end
131
+
132
+ def queue_atomic(&block)
133
+ ensure_exclusive_atomic!
134
+
135
+ begin
136
+ old_atomic, Thread.current[:nobrainer_atomic] = Thread.current[:nobrainer_atomic], self
137
+ block.call(RethinkDB::RQL.new)
138
+ ensure
139
+ Thread.current[:nobrainer_atomic] = old_atomic
140
+ end
141
+ end
142
+
143
+ def _read_attribute(name)
144
+ ensure_exclusive_atomic!
145
+ value = super
146
+
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
152
+ end
153
+ end
154
+
155
+ def _write_attribute(name, value)
156
+ ensure_exclusive_atomic!
157
+
158
+ case [in_atomic?, value.is_a?(PendingAtomic)]
159
+ when [true, true] then super
160
+ when [true, false] then raise NoBrainer::Error::AtomicBlock.new('Avoid the use of atomic blocks for non atomic operations')
161
+ when [false, true] then raise NoBrainer::Error::AtomicBlock.new('Use atomic blocks for atomic operations')
162
+ when [false, false] then super.tap { _touch_attribute(name) }
163
+ end
164
+ end
165
+
166
+ def assign_attributes(attrs, options={})
167
+ ensure_exclusive_atomic!
168
+ super
169
+ end
170
+
171
+ def save?(options={})
172
+ # TODO allow reload => true as an option to save+reload in a single op.
173
+ raise NoBrainer::Error::AtomicBlock.new('You may persist documents only outside of queue_atomic blocks') if in_atomic?
174
+ super.tap do |saved|
175
+ if saved
176
+ @_attributes.each do |attr, value|
177
+ next unless value.is_a?(PendingAtomic)
178
+ @_attributes[attr] = value.class.new(self, attr, nil, false)
179
+ end
180
+ end
181
+ end
182
+ end
183
+
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
+ module ClassMethods
197
+ def persistable_value(k, v, options={})
198
+ v.is_a?(PendingAtomic) ? v.compile_rql_value(options[:rql_doc]) : super
199
+ end
200
+ end
201
+ 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,6 +1,7 @@
1
1
  module NoBrainer::Document::Attributes
2
- VALID_FIELD_OPTIONS = [:index, :default, :type, :real_type, :validates, :required, :unique,
3
- :in, :readonly, :primary_key, :as, :lazy_fetch]
2
+ VALID_FIELD_OPTIONS = [:index, :default, :type, :real_type,
3
+ :validates, :required, :unique, :uniq, :format, :in,
4
+ :readonly, :primary_key, :as, :lazy_fetch]
4
5
  RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] \
5
6
  + NoBrainer::DecoratedSymbol::MODIFIERS.keys
6
7
  extend ActiveSupport::Concern
@@ -25,6 +26,14 @@ module NoBrainer::Document::Attributes
25
26
  Hash[readable_attributes.map { |k| [k, read_attribute(k)] }].with_indifferent_access.freeze
26
27
  end
27
28
 
29
+ def _read_attribute(name)
30
+ @_attributes[name]
31
+ end
32
+
33
+ def _write_attribute(name, value)
34
+ @_attributes[name] = value
35
+ end
36
+
28
37
  def read_attribute(name)
29
38
  __send__("#{name}")
30
39
  end
@@ -91,7 +100,7 @@ module NoBrainer::Document::Attributes
91
100
  module ClassMethods
92
101
  def new_from_db(attrs, options={})
93
102
  options = options.reverse_merge(:pristine => true, :from_db => true)
94
- klass_from_attrs(attrs).new(attrs, options) if attrs
103
+ model_from_attrs(attrs).new(attrs, options) if attrs
95
104
  end
96
105
 
97
106
  def inherited(subclass)
@@ -103,8 +112,8 @@ module NoBrainer::Document::Attributes
103
112
  # Using a layer so the user can use super when overriding these methods
104
113
  attr = attr.to_s
105
114
  inject_in_layer :attributes do
106
- define_method("#{attr}=") { |value| @_attributes[attr] = value }
107
- define_method("#{attr}") { @_attributes[attr] }
115
+ define_method("#{attr}=") { |value| _write_attribute(attr, value) }
116
+ define_method("#{attr}") { _read_attribute(attr) }
108
117
  end
109
118
  end
110
119
 
@@ -116,9 +125,9 @@ module NoBrainer::Document::Attributes
116
125
  raise "Cannot use a reserved field attr: #{attr}"
117
126
  end
118
127
 
119
- ([self] + descendants).each do |klass|
120
- klass.fields[attr] ||= {}
121
- klass.fields[attr].deep_merge!(options)
128
+ ([self] + descendants).each do |model|
129
+ model.fields[attr] ||= {}
130
+ model.fields[attr].deep_merge!(options)
122
131
  end
123
132
 
124
133
  _field(attr, self.fields[attr])
@@ -136,8 +145,8 @@ module NoBrainer::Document::Attributes
136
145
 
137
146
  _remove_field(attr, options)
138
147
 
139
- ([self] + descendants).each do |klass|
140
- klass.fields.delete(attr)
148
+ ([self] + descendants).each do |model|
149
+ model.fields.delete(attr)
141
150
  end
142
151
  end
143
152
 
@@ -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
 
@@ -22,6 +22,6 @@ module NoBrainer::Document::Core
22
22
  extend ActiveModel::Naming
23
23
  extend ActiveModel::Translation
24
24
 
25
- NoBrainer::Document::Core._all << self
25
+ NoBrainer::Document::Core._all << self unless self.name =~ /^NoBrainer::/
26
26
  end
27
27
  end
@@ -5,7 +5,10 @@ module NoBrainer::Document::Criteria
5
5
  self.class.selector_for(pk_value)
6
6
  end
7
7
 
8
- included { cattr_accessor :default_scope_proc, :instance_accessor => false }
8
+ included do
9
+ cattr_accessor :default_scope_proc, :instance_accessor => false
10
+ cattr_accessor :perf_warnings_disabled, :instance_accessor => false
11
+ end
9
12
 
10
13
  module ClassMethods
11
14
  delegate :to_rql, # Core
@@ -27,7 +30,7 @@ module NoBrainer::Document::Criteria
27
30
  :to => :all
28
31
 
29
32
  def all
30
- NoBrainer::Criteria.new(:klass => self)
33
+ NoBrainer::Criteria.new(:model => self)
31
34
  end
32
35
 
33
36
  def scope(name, criteria=nil, &block)
@@ -59,5 +62,9 @@ module NoBrainer::Document::Criteria
59
62
  raise NoBrainer::Error::DocumentNotFound, "#{self} #{pk_name}: #{pk} not found" unless doc
60
63
  end
61
64
  end
65
+
66
+ def disable_perf_warnings
67
+ self.perf_warnings_disabled = true
68
+ end
62
69
  end
63
70
  end
@@ -17,9 +17,7 @@ module NoBrainer::Document::Dirty
17
17
  def clear_dirtiness(options={})
18
18
  if options[:keep_ivars] && options[:missing_attributes].try(:[], :pluck)
19
19
  attrs = options[:missing_attributes][:pluck].keys
20
- @_old_attributes = @_old_attributes.reject { |k,v| attrs.include?(k) }
21
- else
22
- @_old_attributes = {}.with_indifferent_access
20
+ @_old_attributes = @_old_attributes.reject { |k,v| attrs.include?(k) } else @_old_attributes = {}.with_indifferent_access
23
21
  end
24
22
 
25
23
  @_old_attributes_keys = @_attributes.keys # to track undefined -> nil changes
@@ -33,10 +31,14 @@ module NoBrainer::Document::Dirty
33
31
  changes.keys
34
32
  end
35
33
 
34
+ def read_attribute_for_change(attr)
35
+ read_attribute(attr)
36
+ end
37
+
36
38
  def changes
37
39
  result = {}.with_indifferent_access
38
40
  @_old_attributes.each do |attr, old_value|
39
- current_value = read_attribute(attr)
41
+ current_value = read_attribute_for_change(attr)
40
42
  if current_value != old_value || !@_old_attributes_keys.include?(attr)
41
43
  result[attr] = [old_value, current_value]
42
44
  end
@@ -48,7 +50,7 @@ module NoBrainer::Document::Dirty
48
50
  attr = args.first
49
51
  current_value = begin
50
52
  case args.size
51
- when 1 then assert_access_field(attr); read_attribute(attr)
53
+ when 1 then assert_access_field(attr); read_attribute_for_change(attr)
52
54
  when 2 then args.last
53
55
  else raise
54
56
  end
@@ -61,6 +63,19 @@ module NoBrainer::Document::Dirty
61
63
  end
62
64
  end
63
65
 
66
+ def _read_attribute(name)
67
+ super.tap do |value|
68
+ # This take care of string/arrays/hashes that could change without going
69
+ # through the setter.
70
+ attribute_may_change(name, value) if value.respond_to?(:size)
71
+ end
72
+ end
73
+
74
+ def _write_attribute(name, value)
75
+ attribute_may_change(name)
76
+ super
77
+ end
78
+
64
79
  module ClassMethods
65
80
  def _field(attr, options={})
66
81
  super
@@ -69,7 +84,7 @@ module NoBrainer::Document::Dirty
69
84
  inject_in_layer :dirty_tracking do
70
85
  define_method("#{attr}_change") do
71
86
  if @_old_attributes.has_key?(attr)
72
- result = [@_old_attributes[attr], read_attribute(attr)]
87
+ result = [@_old_attributes[attr], read_attribute_for_change(attr)]
73
88
  result if result.first != result.last || !@_old_attributes_keys.include?(attr)
74
89
  end
75
90
  end
@@ -79,20 +94,7 @@ module NoBrainer::Document::Dirty
79
94
  end
80
95
 
81
96
  define_method("#{attr}_was") do
82
- @_old_attributes.has_key?(attr) ? @_old_attributes[attr] : read_attribute(attr)
83
- end
84
-
85
- define_method("#{attr}") do
86
- super().tap do |value|
87
- # This take care of string/arrays/hashes that could change without going
88
- # through the setter.
89
- attribute_may_change(attr, value) if value.respond_to?(:size)
90
- end
91
- end
92
-
93
- define_method("#{attr}=") do |value|
94
- attribute_may_change(attr)
95
- super(value)
97
+ @_old_attributes.has_key?(attr) ? @_old_attributes[attr] : read_attribute_for_change(attr)
96
98
  end
97
99
  end
98
100
  end
@@ -103,8 +105,6 @@ module NoBrainer::Document::Dirty
103
105
  remove_method("#{attr}_change")
104
106
  remove_method("#{attr}_changed?")
105
107
  remove_method("#{attr}_was")
106
- remove_method("#{attr}=")
107
- remove_method("#{attr}")
108
108
  end
109
109
  end
110
110
  end
@@ -2,23 +2,11 @@ module NoBrainer::Document::DynamicAttributes
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def read_attribute(name)
5
- if self.respond_to?("#{name}")
6
- super
7
- else
8
- assert_access_field(name)
9
- @_attributes[name].tap { |value| attribute_may_change(name, value) if value.respond_to?(:size) }
10
- end
5
+ self.respond_to?("#{name}") ? super : _read_attribute(name)
11
6
  end
12
7
 
13
8
  def write_attribute(name, value)
14
- if self.respond_to?("#{name}=")
15
- super
16
- else
17
- attribute_may_change(name)
18
- @_attributes[name] = value
19
- clear_missing_field(name)
20
- value
21
- end
9
+ self.respond_to?("#{name}=") ? super : _write_attribute(name, value)
22
10
  end
23
11
 
24
12
  def readable_attributes
@@ -17,6 +17,7 @@ module NoBrainer::Document::Id
17
17
 
18
18
  def ==(other)
19
19
  return super unless self.class == other.class
20
+ return self.equal?(other) if in_atomic?
20
21
  !pk_value.nil? && pk_value == other.pk_value
21
22
  end
22
23
  alias_method :eql?, :==
@@ -0,0 +1,83 @@
1
+ class NoBrainer::Document::Index::Index < Struct.new(
2
+ :model, :name, :aliased_name, :kind, :what, :external, :geo, :multi, :meta)
3
+
4
+ MetaStore = NoBrainer::Document::Index::MetaStore
5
+
6
+ def initialize(*args)
7
+ super
8
+
9
+ self.name = self.name.to_sym
10
+ self.aliased_name = self.aliased_name.to_sym
11
+ self.external = !!self.external
12
+ self.geo = !!self.geo
13
+ self.multi = !!self.multi
14
+ end
15
+
16
+ def same_definition?(other)
17
+ # allow name to change through renames
18
+ self.model == other.model &&
19
+ self.geo == other.geo &&
20
+ self.multi == other.multi &&
21
+ self.serialized_rql_proc == other.serialized_rql_proc
22
+ end
23
+
24
+ def human_name
25
+ index_name = "index #{model}.#{name}"
26
+ index_name += " as #{aliased_name}" unless name == aliased_name
27
+ index_name
28
+ end
29
+
30
+ def rql_proc
31
+ case kind
32
+ when :single then ->(doc) { doc[model.lookup_field_alias(what)] }
33
+ when :compound then ->(doc) { what.map { |field| doc[model.lookup_field_alias(field)] } }
34
+ when :proc then what # TODO XXX not translating the field aliases
35
+ end
36
+ end
37
+
38
+ def serialized_rql_proc
39
+ meta.try(:rql_function) || (rql_proc && NoBrainer::RQL.rql_proc_as_json(rql_proc))
40
+ end
41
+
42
+ def show_op(verb, options={})
43
+ color = case verb
44
+ when :create then "\e[1;32m" # green
45
+ when :delete then "\e[1;31m" # red
46
+ when :update then "\e[1;33m" # yellow
47
+ end
48
+ STDERR.puts "[NoBrainer] #{color}#{verb.to_s.capitalize} #{human_name}\e[0m" if options[:verbose]
49
+ end
50
+
51
+ def create(options={})
52
+ show_op(:create, options)
53
+
54
+ opt = {}
55
+ opt[:multi] = true if multi
56
+ opt[:geo] = true if geo
57
+
58
+ NoBrainer::RQL.reset_lambda_var_counter
59
+ NoBrainer.run(model.rql_table.index_create(aliased_name, opt, &rql_proc))
60
+
61
+ MetaStore.on(model.database_name) do
62
+ MetaStore.create(:table_name => model.table_name, :index_name => aliased_name,
63
+ :rql_function => serialized_rql_proc)
64
+ end
65
+ end
66
+
67
+ def delete(options={})
68
+ show_op(:delete, options)
69
+
70
+ NoBrainer.run(model.rql_table.index_drop(aliased_name))
71
+
72
+ MetaStore.on(model.database_name) do
73
+ MetaStore.where(:table_name => model.table_name, :index_name => aliased_name).delete_all
74
+ end
75
+ end
76
+
77
+ def update(wanted_index, options={})
78
+ wanted_index.show_op(:update, options)
79
+
80
+ self.delete(options.merge(:verbose => false))
81
+ wanted_index.create(options.merge(:verbose => false))
82
+ end
83
+ end
@@ -0,0 +1,31 @@
1
+ class NoBrainer::Document::Index::MetaStore
2
+ include NoBrainer::Document
3
+ include NoBrainer::Document::Timestamps
4
+
5
+ disable_perf_warnings
6
+
7
+ default_scope ->{ order_by(:created_at) }
8
+
9
+ store_in :database => ->{ Thread.current[:nobrainer_meta_store_db] },
10
+ :table => 'nobrainer_index_meta'
11
+
12
+ field :table_name, :type => String, :required => true
13
+ field :index_name, :type => String, :required => true
14
+ field :rql_function, :type => String, :required => true
15
+
16
+ def rql_function=(value)
17
+ super(JSON.dump(value))
18
+ end
19
+
20
+ def rql_function
21
+ JSON.load(super)
22
+ end
23
+
24
+ def self.on(db_name, &block)
25
+ old_db_name = Thread.current[:nobrainer_meta_store_db]
26
+ Thread.current[:nobrainer_meta_store_db] = db_name
27
+ NoBrainer.with(:auto_create_tables => true) { block.call }
28
+ ensure
29
+ Thread.current[:nobrainer_meta_store_db] = old_db_name
30
+ end
31
+ end