massive_record 0.2.0 → 0.2.1.rc1

Sign up to get free protection for your applications and to get access to all the features.
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