massive_record 0.2.1 → 0.2.2.rc1

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 (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
+