massive_record 0.2.1 → 0.2.2.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. data/CHANGELOG.md +58 -2
  2. data/Gemfile.lock +17 -17
  3. data/README.md +98 -41
  4. data/lib/massive_record.rb +2 -1
  5. data/lib/massive_record/adapters/thrift/hbase/hbase.rb +2425 -2154
  6. data/lib/massive_record/adapters/thrift/hbase/hbase_constants.rb +3 -3
  7. data/lib/massive_record/adapters/thrift/hbase/hbase_types.rb +195 -195
  8. data/lib/massive_record/adapters/thrift/row.rb +35 -4
  9. data/lib/massive_record/adapters/thrift/table.rb +49 -12
  10. data/lib/massive_record/orm/attribute_methods.rb +77 -5
  11. data/lib/massive_record/orm/attribute_methods/cast_numbers_on_write.rb +24 -0
  12. data/lib/massive_record/orm/attribute_methods/dirty.rb +18 -0
  13. data/lib/massive_record/orm/attribute_methods/time_zone_conversion.rb +24 -3
  14. data/lib/massive_record/orm/attribute_methods/write.rb +8 -1
  15. data/lib/massive_record/orm/base.rb +62 -8
  16. data/lib/massive_record/orm/column.rb +7 -11
  17. data/lib/massive_record/orm/default_id.rb +1 -1
  18. data/lib/massive_record/orm/embedded.rb +66 -0
  19. data/lib/massive_record/orm/errors.rb +17 -0
  20. data/lib/massive_record/orm/finders.rb +124 -71
  21. data/lib/massive_record/orm/finders/rescue_missing_table_on_find.rb +1 -1
  22. data/lib/massive_record/orm/finders/scope.rb +58 -34
  23. data/lib/massive_record/orm/id_factory.rb +22 -105
  24. data/lib/massive_record/orm/id_factory/atomic_incrementation.rb +117 -0
  25. data/lib/massive_record/orm/id_factory/timestamp.rb +60 -0
  26. data/lib/massive_record/orm/identity_map.rb +256 -0
  27. data/lib/massive_record/orm/log_subscriber.rb +18 -0
  28. data/lib/massive_record/orm/observer.rb +69 -0
  29. data/lib/massive_record/orm/persistence.rb +47 -119
  30. data/lib/massive_record/orm/persistence/operations.rb +100 -0
  31. data/lib/massive_record/orm/persistence/operations/atomic_operation.rb +71 -0
  32. data/lib/massive_record/orm/persistence/operations/destroy.rb +17 -0
  33. data/lib/massive_record/orm/persistence/operations/embedded/destroy.rb +26 -0
  34. data/lib/massive_record/orm/persistence/operations/embedded/insert.rb +27 -0
  35. data/lib/massive_record/orm/persistence/operations/embedded/operation_helpers.rb +66 -0
  36. data/lib/massive_record/orm/persistence/operations/embedded/reload.rb +39 -0
  37. data/lib/massive_record/orm/persistence/operations/embedded/update.rb +29 -0
  38. data/lib/massive_record/orm/persistence/operations/insert.rb +19 -0
  39. data/lib/massive_record/orm/persistence/operations/reload.rb +26 -0
  40. data/lib/massive_record/orm/persistence/operations/suppress.rb +15 -0
  41. data/lib/massive_record/orm/persistence/operations/table_operation_helpers.rb +106 -0
  42. data/lib/massive_record/orm/persistence/operations/update.rb +25 -0
  43. data/lib/massive_record/orm/query_instrumentation.rb +26 -49
  44. data/lib/massive_record/orm/raw_data.rb +47 -0
  45. data/lib/massive_record/orm/relations.rb +4 -0
  46. data/lib/massive_record/orm/relations/interface.rb +134 -0
  47. data/lib/massive_record/orm/relations/metadata.rb +58 -12
  48. data/lib/massive_record/orm/relations/proxy.rb +17 -12
  49. data/lib/massive_record/orm/relations/proxy/embedded_in.rb +54 -0
  50. data/lib/massive_record/orm/relations/proxy/embedded_in_polymorphic.rb +15 -0
  51. data/lib/massive_record/orm/relations/proxy/embeds_many.rb +215 -0
  52. data/lib/massive_record/orm/relations/proxy/references_many.rb +112 -88
  53. data/lib/massive_record/orm/relations/proxy/references_one.rb +1 -1
  54. data/lib/massive_record/orm/relations/proxy/references_one_polymorphic.rb +1 -1
  55. data/lib/massive_record/orm/relations/proxy_collection.rb +84 -0
  56. data/lib/massive_record/orm/schema/column_family.rb +3 -2
  57. data/lib/massive_record/orm/schema/{column_interface.rb → embedded_interface.rb} +38 -4
  58. data/lib/massive_record/orm/schema/field.rb +2 -0
  59. data/lib/massive_record/orm/schema/table_interface.rb +19 -2
  60. data/lib/massive_record/orm/single_table_inheritance.rb +37 -2
  61. data/lib/massive_record/orm/timestamps.rb +17 -7
  62. data/lib/massive_record/orm/validations.rb +4 -0
  63. data/lib/massive_record/orm/validations/associated.rb +50 -0
  64. data/lib/massive_record/rails/railtie.rb +31 -0
  65. data/lib/massive_record/version.rb +1 -1
  66. data/lib/massive_record/wrapper/cell.rb +8 -1
  67. data/massive_record.gemspec +4 -4
  68. data/spec/adapter/thrift/atomic_increment_spec.rb +16 -0
  69. data/spec/adapter/thrift/table_find_spec.rb +14 -2
  70. data/spec/adapter/thrift/table_spec.rb +6 -6
  71. data/spec/adapter/thrift/utf8_encoding_of_id_spec.rb +71 -0
  72. data/spec/orm/cases/attribute_methods_spec.rb +215 -22
  73. data/spec/orm/cases/auto_generate_id_spec.rb +1 -1
  74. data/spec/orm/cases/change_id_spec.rb +62 -0
  75. data/spec/orm/cases/default_id_spec.rb +25 -6
  76. data/spec/orm/cases/default_values_spec.rb +6 -3
  77. data/spec/orm/cases/dirty_spec.rb +150 -102
  78. data/spec/orm/cases/embedded_spec.rb +250 -0
  79. data/spec/orm/cases/{finder_default_scope.rb → finder_default_scope_spec.rb} +4 -0
  80. data/spec/orm/cases/finder_scope_spec.rb +96 -29
  81. data/spec/orm/cases/finders_spec.rb +57 -10
  82. data/spec/orm/cases/id_factory/atomic_incrementation_spec.rb +72 -0
  83. data/spec/orm/cases/id_factory/timestamp_spec.rb +61 -0
  84. data/spec/orm/cases/identity_map/identity_map_spec.rb +357 -0
  85. data/spec/orm/cases/identity_map/middleware_spec.rb +74 -0
  86. data/spec/orm/cases/log_subscriber_spec.rb +15 -2
  87. data/spec/orm/cases/observing_spec.rb +61 -0
  88. data/spec/orm/cases/persistence_spec.rb +151 -60
  89. data/spec/orm/cases/raw_data_spec.rb +58 -0
  90. data/spec/orm/cases/single_table_inheritance_spec.rb +58 -2
  91. data/spec/orm/cases/table_spec.rb +3 -3
  92. data/spec/orm/cases/time_zone_awareness_spec.rb +27 -0
  93. data/spec/orm/cases/timestamps_spec.rb +23 -109
  94. data/spec/orm/cases/validation_spec.rb +9 -0
  95. data/spec/orm/models/address.rb +5 -1
  96. data/spec/orm/models/address_with_timestamp.rb +12 -0
  97. data/spec/orm/models/car.rb +5 -0
  98. data/spec/orm/models/person.rb +13 -1
  99. data/spec/orm/models/person_with_timestamp.rb +4 -2
  100. data/spec/orm/models/test_class.rb +1 -0
  101. data/spec/orm/persistence/operations/atomic_operation_spec.rb +58 -0
  102. data/spec/orm/persistence/operations/destroy_spec.rb +22 -0
  103. data/spec/orm/persistence/operations/embedded/destroy_spec.rb +71 -0
  104. data/spec/orm/persistence/operations/embedded/insert_spec.rb +59 -0
  105. data/spec/orm/persistence/operations/embedded/operation_helpers_spec.rb +92 -0
  106. data/spec/orm/persistence/operations/embedded/reload_spec.rb +67 -0
  107. data/spec/orm/persistence/operations/embedded/update_spec.rb +60 -0
  108. data/spec/orm/persistence/operations/insert_spec.rb +31 -0
  109. data/spec/orm/persistence/operations/reload_spec.rb +48 -0
  110. data/spec/orm/persistence/operations/suppress_spec.rb +17 -0
  111. data/spec/orm/persistence/operations/table_operation_helpers_spec.rb +98 -0
  112. data/spec/orm/persistence/operations/update_spec.rb +25 -0
  113. data/spec/orm/persistence/operations_spec.rb +58 -0
  114. data/spec/orm/relations/interface_spec.rb +188 -0
  115. data/spec/orm/relations/metadata_spec.rb +92 -15
  116. data/spec/orm/relations/proxy/embedded_in_polymorphic_spec.rb +37 -0
  117. data/spec/orm/relations/proxy/embedded_in_spec.rb +66 -0
  118. data/spec/orm/relations/proxy/embeds_many_spec.rb +651 -0
  119. data/spec/orm/relations/proxy/references_many_spec.rb +466 -2
  120. data/spec/orm/schema/column_family_spec.rb +21 -0
  121. data/spec/orm/schema/embedded_interface_spec.rb +181 -0
  122. data/spec/orm/schema/field_spec.rb +7 -0
  123. data/spec/orm/schema/table_interface_spec.rb +31 -1
  124. data/spec/shared/orm/id_factories.rb +44 -0
  125. data/spec/shared/orm/model_with_timestamps.rb +132 -0
  126. data/spec/shared/orm/persistence/a_persistence_embedded_operation_class.rb +3 -0
  127. data/spec/shared/orm/persistence/a_persistence_operation_class.rb +11 -0
  128. data/spec/shared/orm/persistence/a_persistence_table_operation_class.rb +11 -0
  129. data/spec/shared/orm/relations/proxy.rb +9 -2
  130. data/spec/spec_helper.rb +9 -0
  131. data/spec/support/mock_massive_record_connection.rb +2 -1
  132. metadata +106 -21
  133. data/spec/orm/cases/column_spec.rb +0 -49
  134. data/spec/orm/cases/id_factory_spec.rb +0 -92
  135. data/spec/orm/schema/column_interface_spec.rb +0 -136
@@ -42,6 +42,24 @@ module MassiveRecord
42
42
  debug " " + [name, description, options].compact.join(" ")
43
43
  end
44
44
 
45
+ def identity_map(event)
46
+ return unless logger.debug?
47
+
48
+ payload = event.payload
49
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
50
+ records = payload[:records]
51
+ ids = "id(s): #{records.collect(&:id)}"
52
+
53
+ if odd?
54
+ name = color(name, CYAN, true)
55
+ description = color(description, nil, true)
56
+ else
57
+ name = color(name, MAGENTA, true)
58
+ end
59
+
60
+ debug " " + [name, ids].compact.join(" ")
61
+ end
62
+
45
63
  def query(event)
46
64
  return unless logger.debug?
47
65
 
@@ -0,0 +1,69 @@
1
+ module MassiveRecord
2
+ module ORM
3
+
4
+ #
5
+ # MassiveRecord Observer. Greatly influenced by ActiveRecord's
6
+ # way of doing callbacks, thus should feel familiar to most people.
7
+ #
8
+ # NOTE that if you are using rails you should add observers to into
9
+ # your application.rb configuration file like this:
10
+ #
11
+ # config.massive_record.observers = :person_observer, :audit_observer
12
+ #
13
+ # This will ensure that observers are loaded correctly. If you are not
14
+ # using rails you can do: MassiveRecord::ORM::Base.instantiate_observers
15
+ # after your application has been initialized.
16
+ #
17
+ # Example of usage:
18
+ #
19
+ # class Person < MassiveRecord::ORM::Table
20
+ # column_family :info do
21
+ # field :name
22
+ # end
23
+ # end
24
+ #
25
+ # class PersonObserver < MassiveRecord::ORM::Observer
26
+ # def after_save(saved_person_record)
27
+ # # do something here after people are being saved
28
+ # end
29
+ # end
30
+ #
31
+ # class AuditObserver < MassiveRecord::ORM::Observer
32
+ # observe :person, :and, :other, :classes
33
+ #
34
+ # def after_save(saved_person_record)
35
+ # # do something here after people are being saved
36
+ # end
37
+ # end
38
+ #
39
+ class Observer < ActiveModel::Observer
40
+ protected
41
+
42
+ def observed_classes
43
+ klasses = super
44
+ klasses + klasses.map { |klass| klass.descendants }.flatten
45
+ end
46
+
47
+ def add_observer!(klass)
48
+ super
49
+ define_callbacks klass
50
+ end
51
+
52
+ def define_callbacks(klass)
53
+ observer = self
54
+ observer_name = observer.class.name.underscore.gsub('/', '__')
55
+
56
+ MassiveRecord::ORM::Callbacks::CALLBACKS.each do |callback|
57
+ next unless respond_to?(callback)
58
+ callback_meth = :"_notify_#{observer_name}_for_#{callback}"
59
+ unless klass.respond_to?(callback_meth)
60
+ klass.send(:define_method, callback_meth) do |&block|
61
+ observer.send(callback, self, &block)
62
+ end
63
+ klass.send(callback, callback_meth)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,3 +1,5 @@
1
+ require 'massive_record/orm/persistence/operations'
2
+
1
3
  module MassiveRecord
2
4
  module ORM
3
5
  module Persistence
@@ -14,42 +16,6 @@ module MassiveRecord
14
16
  def destroy_all
15
17
  all.each { |record| record.destroy }
16
18
  end
17
-
18
-
19
- #
20
- # Iterates over tables and column families and ensure that we
21
- # have what we need
22
- #
23
- def ensure_that_we_have_table_and_column_families! # :nodoc:
24
- #
25
- # TODO: Can we skip checking if it exists at all, and instead, rescue it if it does not?
26
- #
27
- hbase_create_table! unless table.exists?
28
- raise ColumnFamiliesMissingError.new(self, calculate_missing_family_names) if calculate_missing_family_names.any?
29
- end
30
-
31
-
32
- private
33
-
34
- #
35
- # Creates table for this ORM class
36
- #
37
- def hbase_create_table!
38
- missing_family_names = calculate_missing_family_names
39
- table.create_column_families(missing_family_names) unless missing_family_names.empty?
40
- table.save
41
- end
42
-
43
- #
44
- # Calculate which column families are missing in the database in
45
- # context of what the schema instructs.
46
- #
47
- def calculate_missing_family_names
48
- existing_family_names = table.fetch_column_families.collect(&:name) rescue []
49
- expected_family_names = column_families ? column_families.collect(&:name) : []
50
-
51
- expected_family_names.collect(&:to_s) - existing_family_names.collect(&:to_s)
52
- end
53
19
  end
54
20
 
55
21
 
@@ -67,7 +33,7 @@ module MassiveRecord
67
33
 
68
34
 
69
35
  def reload
70
- self.attributes_raw = self.class.find(id).attributes if persisted?
36
+ Operations.reload(self).execute
71
37
  self
72
38
  end
73
39
 
@@ -101,11 +67,31 @@ module MassiveRecord
101
67
  end
102
68
 
103
69
  def destroy
104
- @destroyed = (persisted? ? row_for_record.destroy : true) and freeze
70
+ @destroyed = (persisted? ? do_destroy : true) and freeze
105
71
  end
106
72
  alias_method :delete, :destroy
107
73
 
108
74
 
75
+ def change_id!(new_id)
76
+ old_id, self.id = id, new_id
77
+
78
+ @new_record = true
79
+ unless save
80
+ raise <<-TXT
81
+ Unable to save #{self.class} with updated id '#{new_id}'.
82
+ Old id '#{old_id}' was not deleted so in theory nothing should be changed in the database.
83
+ TXT
84
+ end
85
+
86
+ unless self.class.find(old_id).destroy
87
+ raise <<-TXT
88
+ Unable to destroy #{self.class} with id '#{old_id}'.
89
+ You now how duplicate records in the database. New id is: '#{new_id}.'
90
+ TXT
91
+ end
92
+
93
+ reload
94
+ end
109
95
 
110
96
 
111
97
  def increment(attr_name, by = 1)
@@ -119,21 +105,11 @@ module MassiveRecord
119
105
  increment(attr_name, by).update_attribute(attr_name, self[attr_name])
120
106
  end
121
107
 
122
- # Atomic increment of an attribute. Please note that it's the
123
- # adapter (or the wrapper) which needs to guarantee that the update
124
- # is atomic, and as of writing this the Thrift adapter / wrapper does
125
- # not do this anatomic.
126
108
  def atomic_increment!(attr_name, by = 1)
127
- self.class.ensure_that_we_have_table_and_column_families!
128
- attr_name = attr_name.to_s
129
-
130
- ensure_proper_binary_integer_representation(attr_name)
131
-
132
- self[attr_name] = row_for_record.atomic_increment(attributes_schema[attr_name].unique_name, by)
133
- @new_record = false
134
- self[attr_name]
109
+ atomic_operation(:increment, attr_name, by)
135
110
  end
136
111
 
112
+
137
113
  def decrement(attr_name, by = 1)
138
114
  raise NotNumericalFieldError unless attributes_schema[attr_name.to_s].type == :integer
139
115
  self[attr_name] ||= 0
@@ -144,6 +120,11 @@ module MassiveRecord
144
120
  def decrement!(attr_name, by = 1)
145
121
  decrement(attr_name, by).update_attribute(attr_name, self[attr_name])
146
122
  end
123
+
124
+ def atomic_decrement!(attr_name, by = 1)
125
+ atomic_operation(:decrement, attr_name, by)
126
+ end
127
+
147
128
 
148
129
 
149
130
  private
@@ -151,94 +132,41 @@ module MassiveRecord
151
132
 
152
133
  def create_or_update
153
134
  raise ReadOnlyRecord if readonly?
135
+
154
136
  !!(new_record? ? create : update)
155
137
  end
156
138
 
157
139
  def create
158
- self.class.ensure_that_we_have_table_and_column_families!
159
-
160
- raise RecordNotUnique if check_record_uniqueness_on_create && self.class.exists?(id)
161
-
162
- if saved = store_record_to_database('create')
163
- @new_record = false
140
+ Operations.insert(self).execute.tap do |saved|
141
+ @new_record = false if saved
164
142
  end
165
- saved
166
143
  end
167
144
 
168
- def update(attribute_names_to_update = attributes.keys)
169
- self.class.ensure_that_we_have_table_and_column_families!
170
-
171
- store_record_to_database('update', attribute_names_to_update)
145
+ def update(attribute_names_to_update = attributes_with_embedded)
146
+ Operations.update(self, :attribute_names_to_update => attribute_names_to_update).execute
172
147
  end
173
148
 
174
-
175
-
176
-
177
- #
178
- # Takes care of the actual storing of the record to the database
179
- # Both update and create is using this
180
- #
181
- def store_record_to_database(action, attribute_names_to_update = [])
182
- row = row_for_record
183
- row.values = attributes_to_row_values_hash(attribute_names_to_update)
184
- row.save
149
+ def do_destroy
150
+ Operations.destroy(self).execute
185
151
  end
186
152
 
187
-
188
-
189
153
  #
190
- # Returns a Wrapper::Row class which we can manipulate this
191
- # record in the database with
154
+ # Atomic decrement of an attribute. Please note that it's the
155
+ # adapter (or the wrapper) which needs to guarantee that the update
156
+ # is atomic. Thrift adapter is working with atomic decrementation.
192
157
  #
193
- def row_for_record
194
- raise IdMissing.new("You must set an ID before save.") if id.blank?
195
-
196
- MassiveRecord::Wrapper::Row.new({
197
- :id => id,
198
- :table => self.class.table
199
- })
158
+ def atomic_operation(operation, attr_name, by)
159
+ Operations.atomic_operation(self, :operation => operation, :attr_name => attr_name, :by => by).execute
200
160
  end
201
161
 
202
- #
203
- # Returns attributes on a form which Wrapper::Row expects
204
- #
205
- def attributes_to_row_values_hash(only_attr_names = [])
206
- values = Hash.new { |hash, key| hash[key] = Hash.new }
207
-
208
- attributes_schema.each do |attr_name, orm_field|
209
- next unless only_attr_names.empty? || only_attr_names.include?(attr_name)
210
- values[orm_field.column_family.name][orm_field.column] = orm_field.encode(self[attr_name])
211
- end
212
162
 
213
- values
214
- end
215
163
 
216
164
  #
217
- # To cope with the problem which arises when you ask to
218
- # do atomic incrementation of an attribute and that attribute
219
- # has a string representation of a number, like "1", instead of
220
- # the binary representation, like "\x00\x00\x00\x00\x00\x00\x00\x01".
165
+ # Gives you all attribute names pluss all known embedded
166
+ # attributes names. Is used if dirty is active.
221
167
  #
222
- # We then need to re-write that string representation into
223
- # hex representation. Now, if you are on a completely new
224
- # database and have never used MassiveRecord before we should not
225
- # need to do this at all; numbers are now stored as hex, but for
226
- # backward compatibility we are doing this.
227
- #
228
- # Now, there is a risk of doing this; if two calls are made to
229
- # atomic_increment! on a record where it's value is a string
230
- # representation this operation might be compromised. Therefor
231
- # you need to enable this feature.
232
- #
233
- def ensure_proper_binary_integer_representation(attr_name)
234
- return if !backward_compatibility_integers_might_be_persisted_as_strings || new_record?
235
-
236
- field = attributes_schema[attr_name]
237
- raise "Not an integer field" unless field.try(:type) == :integer
238
-
239
- if raw_value = self.class.table.get(id, field.column_family.name, field.name)
240
- store_record_to_database('update', [attr_name]) if raw_value =~ /\A\d*\Z/
241
- end
168
+ def attributes_with_embedded
169
+ attributes.keys | relation_proxies_for_embedded.collect { |proxy| proxy.metadata.name }
242
170
  end
243
171
  end
244
172
  end
@@ -0,0 +1,100 @@
1
+ require 'massive_record/orm/persistence/operations/suppress'
2
+
3
+ require 'massive_record/orm/persistence/operations/insert'
4
+ require 'massive_record/orm/persistence/operations/update'
5
+ require 'massive_record/orm/persistence/operations/destroy'
6
+ require 'massive_record/orm/persistence/operations/reload'
7
+ require 'massive_record/orm/persistence/operations/atomic_operation'
8
+
9
+ require 'massive_record/orm/persistence/operations/embedded/insert'
10
+ require 'massive_record/orm/persistence/operations/embedded/update'
11
+ require 'massive_record/orm/persistence/operations/embedded/destroy'
12
+ require 'massive_record/orm/persistence/operations/embedded/reload'
13
+
14
+ module MassiveRecord
15
+ module ORM
16
+ module Persistence
17
+
18
+ #
19
+ # The persistence Operations are in charge of inserting,
20
+ # updating and destroying records.
21
+ #
22
+ # It's reason for even existing is that we need to
23
+ # do these operations differently based on if we are
24
+ # saving a record which has a Table as it's class or saving
25
+ # an embedded record.
26
+ #
27
+ # The Persistence module will call upon
28
+ # Operations.insert(record, options) and execute on the returned
29
+ # object. Based on what kind of record we are getting in we
30
+ # can determine what kind of Operation object to return
31
+ #
32
+ module Operations
33
+ class << self
34
+ def suppress
35
+ @suppressed = true
36
+ yield
37
+ ensure
38
+ @suppressed = false
39
+ end
40
+
41
+ def suppressed?
42
+ !!@suppressed
43
+ end
44
+
45
+
46
+ def insert(record, options = {})
47
+ operator_for :insert, record, options
48
+ end
49
+
50
+ def update(record, options = {})
51
+ operator_for :update, record, options
52
+ end
53
+
54
+ def destroy(record, options = {})
55
+ operator_for :destroy, record, options
56
+ end
57
+
58
+ def atomic_operation(record, options = {})
59
+ operator_for :atomic_operation, record, options
60
+ end
61
+
62
+ def reload(record, options = {})
63
+ operator_for :reload, record, options
64
+ end
65
+
66
+ private
67
+
68
+ def operator_for(operation, record, options)
69
+ if suppressed?
70
+ klass = Suppress
71
+ else
72
+ class_parts = [self]
73
+ class_parts << "Embedded" if record.kind_of? ORM::Embedded
74
+ class_parts << operation.to_s.classify
75
+
76
+ klass = class_parts.join("::").constantize
77
+ end
78
+
79
+ klass.new(record, options)
80
+ end
81
+ end
82
+
83
+
84
+ attr_reader :record, :klass, :options
85
+
86
+
87
+ def initialize(record, options = {})
88
+ @record = record
89
+ @klass = record.class
90
+ @options = options
91
+ end
92
+
93
+
94
+ def execute
95
+ raise "Not implemented"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,71 @@
1
+ require 'massive_record/orm/persistence/operations/table_operation_helpers'
2
+
3
+ module MassiveRecord
4
+ module ORM
5
+ module Persistence
6
+ module Operations
7
+ class AtomicOperation
8
+ include Operations, TableOperationHelpers
9
+
10
+ def execute
11
+ raise NotNumericalFieldError unless record.attributes_schema[attr_name].type == :integer
12
+
13
+ ensure_that_we_have_table_and_column_families!
14
+ ensure_proper_binary_integer_representation(attr_name)
15
+ record[attr_name] = row_for_record.send("atomic_#{operation}", record.attributes_schema[attr_name].unique_name, by)
16
+ ensure
17
+ record.instance_variable_set(:@new_record, false)
18
+ end
19
+
20
+
21
+ private
22
+
23
+ #
24
+ # To cope with the problem which arises when you ask to
25
+ # do atomic incrementation / decrementation of an attribute and that attribute
26
+ # has a string representation of a number, like "1", instead of
27
+ # the binary representation, like "\x00\x00\x00\x00\x00\x00\x00\x01".
28
+ #
29
+ # We then need to re-write that string representation into
30
+ # hex representation. Now, if you are on a completely new
31
+ # database and have never used MassiveRecord before we should not
32
+ # need to do this at all; numbers are now stored as hex, but for
33
+ # backward compatibility we are doing this.
34
+ #
35
+ # Now, there is a risk of doing this; if two calls are made to
36
+ # atomic_increment! or atomic_decrement! on a record where it's value is a string
37
+ # representation this operation might be compromised. Therefor
38
+ # you need to enable this feature.
39
+ #
40
+ def ensure_proper_binary_integer_representation(attr_name)
41
+ return if !klass.backward_compatibility_integers_might_be_persisted_as_strings || record.new_record?
42
+
43
+ field = record.attributes_schema[attr_name]
44
+
45
+ if raw_value = klass.table.get(record.id, field.column_family.name, field.name)
46
+ store_record_to_database('update', [attr_name]) if raw_value =~ /\A\d*\Z/
47
+ end
48
+ end
49
+
50
+
51
+ def operation
52
+ options[:operation] or raise "Missing option :operation"
53
+ end
54
+
55
+ def attr_name
56
+ @attr_name ||= if options[:attr_name].present?
57
+ options[:attr_name].to_s
58
+ else
59
+ raise "Missing option :attr_name"
60
+ end
61
+ end
62
+
63
+ def by
64
+ options[:by] or raise "Missing option :by"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+