massive_record 0.2.0 → 0.2.1.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 (68) hide show
  1. data/CHANGELOG.md +43 -4
  2. data/Gemfile.lock +3 -1
  3. data/README.md +5 -0
  4. data/lib/massive_record/adapters/thrift/connection.rb +23 -16
  5. data/lib/massive_record/adapters/thrift/row.rb +13 -33
  6. data/lib/massive_record/adapters/thrift/table.rb +24 -10
  7. data/lib/massive_record/orm/attribute_methods.rb +27 -1
  8. data/lib/massive_record/orm/attribute_methods/dirty.rb +2 -2
  9. data/lib/massive_record/orm/attribute_methods/read.rb +36 -1
  10. data/lib/massive_record/orm/attribute_methods/time_zone_conversion.rb +81 -0
  11. data/lib/massive_record/orm/attribute_methods/write.rb +18 -0
  12. data/lib/massive_record/orm/base.rb +52 -10
  13. data/lib/massive_record/orm/callbacks.rb +1 -1
  14. data/lib/massive_record/orm/default_id.rb +20 -0
  15. data/lib/massive_record/orm/errors.rb +4 -0
  16. data/lib/massive_record/orm/finders.rb +102 -57
  17. data/lib/massive_record/orm/finders/rescue_missing_table_on_find.rb +45 -0
  18. data/lib/massive_record/orm/id_factory.rb +1 -1
  19. data/lib/massive_record/orm/log_subscriber.rb +85 -0
  20. data/lib/massive_record/orm/persistence.rb +82 -37
  21. data/lib/massive_record/orm/query_instrumentation.rb +64 -0
  22. data/lib/massive_record/orm/relations/interface.rb +10 -0
  23. data/lib/massive_record/orm/relations/metadata.rb +2 -0
  24. data/lib/massive_record/orm/relations/proxy/references_one_polymorphic.rb +1 -1
  25. data/lib/massive_record/orm/schema/field.rb +33 -6
  26. data/lib/massive_record/orm/timestamps.rb +1 -1
  27. data/lib/massive_record/orm/validations.rb +2 -2
  28. data/lib/massive_record/rails/controller_runtime.rb +55 -0
  29. data/lib/massive_record/rails/railtie.rb +16 -0
  30. data/lib/massive_record/version.rb +1 -1
  31. data/lib/massive_record/wrapper/cell.rb +32 -3
  32. data/massive_record.gemspec +1 -0
  33. data/spec/{wrapper/cases → adapter/thrift}/adapter_spec.rb +0 -0
  34. data/spec/adapter/thrift/atomic_increment_spec.rb +55 -0
  35. data/spec/{wrapper/cases → adapter/thrift}/connection_spec.rb +0 -10
  36. data/spec/adapter/thrift/table_find_spec.rb +40 -0
  37. data/spec/{wrapper/cases → adapter/thrift}/table_spec.rb +55 -13
  38. data/spec/orm/cases/attribute_methods_spec.rb +6 -1
  39. data/spec/orm/cases/base_spec.rb +18 -4
  40. data/spec/orm/cases/callbacks_spec.rb +1 -1
  41. data/spec/orm/cases/default_id_spec.rb +38 -0
  42. data/spec/orm/cases/default_values_spec.rb +37 -0
  43. data/spec/orm/cases/dirty_spec.rb +25 -1
  44. data/spec/orm/cases/encoding_spec.rb +3 -3
  45. data/spec/orm/cases/finder_default_scope.rb +8 -1
  46. data/spec/orm/cases/finder_scope_spec.rb +2 -2
  47. data/spec/orm/cases/finders_spec.rb +8 -18
  48. data/spec/orm/cases/id_factory_spec.rb +38 -21
  49. data/spec/orm/cases/log_subscriber_spec.rb +133 -0
  50. data/spec/orm/cases/mass_assignment_security_spec.rb +97 -0
  51. data/spec/orm/cases/persistence_spec.rb +132 -27
  52. data/spec/orm/cases/single_table_inheritance_spec.rb +2 -2
  53. data/spec/orm/cases/time_zone_awareness_spec.rb +157 -0
  54. data/spec/orm/cases/timestamps_spec.rb +15 -0
  55. data/spec/orm/cases/validation_spec.rb +2 -2
  56. data/spec/orm/models/model_without_default_id.rb +5 -0
  57. data/spec/orm/models/person.rb +1 -0
  58. data/spec/orm/models/test_class.rb +1 -0
  59. data/spec/orm/relations/interface_spec.rb +2 -2
  60. data/spec/orm/relations/metadata_spec.rb +1 -1
  61. data/spec/orm/relations/proxy/references_many_spec.rb +21 -15
  62. data/spec/orm/relations/proxy/references_one_polymorphic_spec.rb +7 -1
  63. data/spec/orm/relations/proxy/references_one_spec.rb +7 -0
  64. data/spec/orm/schema/field_spec.rb +61 -5
  65. data/spec/support/connection_helpers.rb +2 -1
  66. data/spec/support/mock_massive_record_connection.rb +7 -0
  67. data/spec/support/time_zone_helper.rb +25 -0
  68. metadata +51 -14
@@ -0,0 +1,85 @@
1
+ module MassiveRecord
2
+ module ORM
3
+ class LogSubscriber < ActiveSupport::LogSubscriber
4
+ def self.runtime=(value)
5
+ Thread.current["massive_record_query_runtime"] = value
6
+ end
7
+
8
+ def self.runtime
9
+ Thread.current["massive_record_query_runtime"] ||= 0
10
+ end
11
+
12
+ def self.reset_runtime
13
+ rt, self.runtime = runtime, 0
14
+ rt
15
+ end
16
+
17
+
18
+
19
+ def load(event)
20
+ self.class.runtime += event.duration
21
+
22
+ return unless logger.debug?
23
+
24
+ payload = event.payload
25
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
26
+ description = payload[:description]
27
+ options = payload[:options]
28
+
29
+ if options.present? && options.any?
30
+ options = "options: #{options}"
31
+ else
32
+ options = nil
33
+ end
34
+
35
+ if odd?
36
+ name = color(name, CYAN, true)
37
+ description = color(description, nil, true)
38
+ else
39
+ name = color(name, MAGENTA, true)
40
+ end
41
+
42
+ debug " " + [name, description, options].compact.join(" ")
43
+ end
44
+
45
+ def query(event)
46
+ return unless logger.debug?
47
+
48
+ payload = event.payload
49
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
50
+ description = payload[:description]
51
+ options = payload[:options]
52
+
53
+ if options.present? && options.any?
54
+ options = "options: #{options}"
55
+ else
56
+ options = nil
57
+ end
58
+
59
+ if odd?
60
+ name = color(name, CYAN, true)
61
+ description = color(description, nil, true)
62
+ else
63
+ name = color(name, MAGENTA, true)
64
+ end
65
+
66
+ debug " " + [name, description, options].compact.join(" ")
67
+ end
68
+
69
+
70
+
71
+ def logger
72
+ MassiveRecord::ORM::Base.logger
73
+ end
74
+
75
+
76
+ private
77
+
78
+ def odd?
79
+ @odd = !@odd
80
+ end
81
+ end
82
+
83
+ LogSubscriber.attach_to :massive_record
84
+ end
85
+ end
@@ -3,9 +3,10 @@ module MassiveRecord
3
3
  module Persistence
4
4
  extend ActiveSupport::Concern
5
5
 
6
+
6
7
  module ClassMethods
7
- def create(attributes = {})
8
- new(attributes).tap do |record|
8
+ def create(*args)
9
+ new(*args).tap do |record|
9
10
  record.save
10
11
  end
11
12
  end
@@ -13,6 +14,42 @@ module MassiveRecord
13
14
  def destroy_all
14
15
  all.each { |record| record.destroy }
15
16
  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
16
53
  end
17
54
 
18
55
 
@@ -43,7 +80,7 @@ module MassiveRecord
43
80
  end
44
81
 
45
82
  def update_attribute(attr_name, value)
46
- send("#{attr_name}=", value)
83
+ self[attr_name] = value
47
84
  save(:validate => false)
48
85
  end
49
86
 
@@ -87,12 +124,14 @@ module MassiveRecord
87
124
  # is atomic, and as of writing this the Thrift adapter / wrapper does
88
125
  # not do this anatomic.
89
126
  def atomic_increment!(attr_name, by = 1)
90
- ensure_that_we_have_table_and_column_families!
127
+ self.class.ensure_that_we_have_table_and_column_families!
91
128
  attr_name = attr_name.to_s
92
129
 
93
- row = row_for_record
94
- row.values = attributes_to_row_values_hash([attr_name])
95
- self[attr_name] = row.atomic_increment(attributes_schema[attr_name].unique_name, by).to_i
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]
96
135
  end
97
136
 
98
137
  def decrement(attr_name, by = 1)
@@ -116,18 +155,20 @@ module MassiveRecord
116
155
  end
117
156
 
118
157
  def create
119
- ensure_that_we_have_table_and_column_families!
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)
120
161
 
121
- if saved = store_record_to_database
162
+ if saved = store_record_to_database('create')
122
163
  @new_record = false
123
164
  end
124
165
  saved
125
166
  end
126
167
 
127
168
  def update(attribute_names_to_update = attributes.keys)
128
- ensure_that_we_have_table_and_column_families!
169
+ self.class.ensure_that_we_have_table_and_column_families!
129
170
 
130
- store_record_to_database(attribute_names_to_update)
171
+ store_record_to_database('update', attribute_names_to_update)
131
172
  end
132
173
 
133
174
 
@@ -137,37 +178,13 @@ module MassiveRecord
137
178
  # Takes care of the actual storing of the record to the database
138
179
  # Both update and create is using this
139
180
  #
140
- def store_record_to_database(attribute_names_to_update = [])
181
+ def store_record_to_database(action, attribute_names_to_update = [])
141
182
  row = row_for_record
142
183
  row.values = attributes_to_row_values_hash(attribute_names_to_update)
143
184
  row.save
144
185
  end
145
186
 
146
187
 
147
- #
148
- # Iterates over tables and column families and ensure that we
149
- # have what we need
150
- #
151
- def ensure_that_we_have_table_and_column_families!
152
- if !self.class.connection.tables.include? self.class.table_name
153
- missing_family_names = calculate_missing_family_names
154
- self.class.table.create_column_families(missing_family_names) unless missing_family_names.empty?
155
- self.class.table.save
156
- end
157
-
158
- raise ColumnFamiliesMissingError.new(self.class, calculate_missing_family_names) if !calculate_missing_family_names.empty?
159
- end
160
-
161
- #
162
- # Calculate which column families are missing in the database in
163
- # context of what the schema instructs.
164
- #
165
- def calculate_missing_family_names
166
- existing_family_names = self.class.table.fetch_column_families.collect(&:name) rescue []
167
- expected_family_names = column_families ? column_families.collect(&:name) : []
168
-
169
- expected_family_names.collect(&:to_s) - existing_family_names.collect(&:to_s)
170
- end
171
188
 
172
189
  #
173
190
  # Returns a Wrapper::Row class which we can manipulate this
@@ -190,11 +207,39 @@ module MassiveRecord
190
207
 
191
208
  attributes_schema.each do |attr_name, orm_field|
192
209
  next unless only_attr_names.empty? || only_attr_names.include?(attr_name)
193
- values[orm_field.column_family.name][orm_field.column] = orm_field.encode(send(attr_name))
210
+ values[orm_field.column_family.name][orm_field.column] = orm_field.encode(self[attr_name])
194
211
  end
195
212
 
196
213
  values
197
214
  end
215
+
216
+ #
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".
221
+ #
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
242
+ end
198
243
  end
199
244
  end
200
245
  end
@@ -0,0 +1,64 @@
1
+ module MassiveRecord
2
+ module ORM
3
+ module QueryInstrumentation
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ #
8
+ # do_find is the method which *all* find operations goes
9
+ # through. For instrumentation on the query only see
10
+ # hbase_query_all_first /hbase_query_find
11
+ #
12
+ def do_find(*args)
13
+ ActiveSupport::Notifications.instrument("load.massive_record", {
14
+ :name => [model_name, 'load'].join(' '),
15
+ :description => "",
16
+ :options => args
17
+ }) do
18
+ super
19
+ end
20
+ end
21
+
22
+
23
+
24
+
25
+ private
26
+
27
+ def hbase_query_all_first(type, *args)
28
+ ActiveSupport::Notifications.instrument("find_query.massive_record", {
29
+ :name => [model_name, 'query'].join(' '),
30
+ :description => type,
31
+ :options => args
32
+ }) do
33
+ super
34
+ end
35
+ end
36
+
37
+ def hbase_query_find(what_to_find, options)
38
+ ActiveSupport::Notifications.instrument("find_query.massive_record", {
39
+ :name => [model_name, 'query'].join(' '),
40
+ :description => "find id(s): #{what_to_find}",
41
+ :options => options
42
+ }) do
43
+ super
44
+ end
45
+ end
46
+ end
47
+
48
+
49
+ private
50
+
51
+ def store_record_to_database(action, attribute_names_to_update = [])
52
+ description = action + " id: #{id},"
53
+ description += " attributes: #{attribute_names_to_update.join(', ')}" if attribute_names_to_update.any?
54
+
55
+ ActiveSupport::Notifications.instrument("query.massive_record", {
56
+ :name => [self.class.model_name, 'save'].join(' '),
57
+ :description => description
58
+ }) do
59
+ super
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -146,6 +146,12 @@ module MassiveRecord
146
146
 
147
147
 
148
148
 
149
+ def reload
150
+ reset_relation_proxies
151
+ super
152
+ end
153
+
154
+
149
155
  private
150
156
 
151
157
  def relation_proxy(name)
@@ -168,6 +174,10 @@ module MassiveRecord
168
174
  def relation_proxy_set(name, proxy)
169
175
  @relation_proxy_cache[name.to_s] = proxy
170
176
  end
177
+
178
+ def reset_relation_proxies
179
+ @relation_proxy_cache.values.each(&:reset)
180
+ end
171
181
  end
172
182
  end
173
183
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/array/extract_options'
2
+
1
3
  module MassiveRecord
2
4
  module ORM
3
5
  module Relations
@@ -4,7 +4,7 @@ module MassiveRecord
4
4
  class Proxy
5
5
  class ReferencesOnePolymorphic < Proxy
6
6
  def proxy_target=(proxy_target)
7
- set_foreign_key_and_type_in_proxy_owner(proxy_target.id, proxy_target.class.to_s.underscore) if proxy_target
7
+ set_foreign_key_and_type_in_proxy_owner(proxy_target.id, proxy_target.class.to_s) if proxy_target
8
8
  super(proxy_target)
9
9
  end
10
10
 
@@ -13,9 +13,9 @@ module MassiveRecord
13
13
  :hash => {},
14
14
  :date => lambda { Date.today },
15
15
  :time => lambda { Time.now }
16
- }
16
+ }.freeze
17
17
 
18
- TYPES = TYPES_DEFAULTS_TO.keys
18
+ TYPES = TYPES_DEFAULTS_TO.keys.freeze
19
19
 
20
20
  attr_writer :default
21
21
  attr_accessor :name, :column, :type, :fields, :coder, :allow_nil
@@ -109,11 +109,13 @@ module MassiveRecord
109
109
 
110
110
 
111
111
  def decode(value)
112
+ value = value.force_encoding(Encoding::UTF_8) if utf_8_encoded? && !value.frozen? && value.respond_to?(:force_encoding)
113
+
112
114
  return value if value.nil? || value_is_already_decoded?(value)
113
115
 
114
116
  value = case type
115
117
  when :boolean
116
- value.blank? ? nil : !value.to_s.match(/^(true|1)$/i).nil?
118
+ value.blank? || value == @@encoded_nil_value ? nil : !value.to_s.match(/^(true|1)$/i).nil?
117
119
  when :date
118
120
  value.blank? || value.to_s == "0" ? nil : (Date.parse(value) rescue nil)
119
121
  when :time
@@ -123,7 +125,13 @@ module MassiveRecord
123
125
  value = value.to_s if value.is_a? Symbol
124
126
  coder.load(value)
125
127
  end
126
- when :integer, :float, :array, :hash
128
+ when :integer
129
+ if value =~ /\A\d*\Z/
130
+ coder.load(value) if value.present?
131
+ else
132
+ hex_string_to_integer(value)
133
+ end
134
+ when :float, :array, :hash
127
135
  coder.load(value) if value.present?
128
136
  else
129
137
  raise "Unable to decode #{value}, class: #{value}"
@@ -135,10 +143,11 @@ module MassiveRecord
135
143
  end
136
144
 
137
145
  def encode(value)
138
- if type == :string && !(value.nil? || value == @@encoded_nil_value)
146
+ if value.nil? || should_not_be_encoded?
139
147
  value
140
148
  else
141
- coder.dump(value)
149
+ value = value.try(:utc) if Base.time_zone_aware_attributes && field_affected_by_time_zone_awareness?
150
+ coder.dump(value).to_s
142
151
  end
143
152
  end
144
153
 
@@ -169,6 +178,8 @@ module MassiveRecord
169
178
  def value_is_already_decoded?(value)
170
179
  if type == :string
171
180
  value.is_a?(String) && !(value == @@encoded_null_string || value == @@encoded_nil_value)
181
+ elsif value.acts_like?(type)
182
+ true
172
183
  else
173
184
  classes.include?(value.class)
174
185
  end
@@ -177,6 +188,22 @@ module MassiveRecord
177
188
  def loaded_value_is_of_valid_class?(value)
178
189
  value.nil? || value.is_a?(String) && value == @@encoded_nil_value || value_is_already_decoded?(value)
179
190
  end
191
+
192
+ def field_affected_by_time_zone_awareness?
193
+ type == :time
194
+ end
195
+
196
+ def hex_string_to_integer(string)
197
+ Wrapper::Cell.hex_string_to_integer(string)
198
+ end
199
+
200
+ def utf_8_encoded?
201
+ type != :integer
202
+ end
203
+
204
+ def should_not_be_encoded?
205
+ [:string, :integer].include?(type)
206
+ end
180
207
  end
181
208
  end
182
209
  end