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
@@ -9,6 +9,24 @@ module MassiveRecord
9
9
  end
10
10
 
11
11
 
12
+ module ClassMethods
13
+ protected
14
+
15
+ def define_method_attribute=(attr_name)
16
+ if attr_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP
17
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__
18
+ def #{attr_name}=(value)
19
+ write_attribute('#{attr_name}', value)
20
+ end
21
+ RUBY
22
+ else
23
+ generated_attribute_methods.send(:define_method, "#{attr_name}=") do |value|
24
+ write_attribute(attr_name, value)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
12
30
  def write_attribute(attr_name, value)
13
31
  @attributes[attr_name.to_s] = value
14
32
  end
@@ -4,6 +4,7 @@ require 'active_support/core_ext/class/attribute'
4
4
  require 'active_support/core_ext/class/subclasses'
5
5
  require 'active_support/core_ext/module'
6
6
  require 'active_support/core_ext/string'
7
+ require 'active_support/core_ext/array'
7
8
  require 'active_support/memoizable'
8
9
 
9
10
  require 'massive_record/orm/schema'
@@ -13,7 +14,9 @@ require 'massive_record/orm/config'
13
14
  require 'massive_record/orm/relations'
14
15
  require 'massive_record/orm/finders'
15
16
  require 'massive_record/orm/finders/scope'
17
+ require 'massive_record/orm/finders/rescue_missing_table_on_find'
16
18
  require 'massive_record/orm/attribute_methods'
19
+ require 'massive_record/orm/attribute_methods/time_zone_conversion'
17
20
  require 'massive_record/orm/attribute_methods/write'
18
21
  require 'massive_record/orm/attribute_methods/read'
19
22
  require 'massive_record/orm/attribute_methods/dirty'
@@ -22,6 +25,8 @@ require 'massive_record/orm/validations'
22
25
  require 'massive_record/orm/callbacks'
23
26
  require 'massive_record/orm/timestamps'
24
27
  require 'massive_record/orm/persistence'
28
+ require 'massive_record/orm/default_id'
29
+ require 'massive_record/orm/query_instrumentation'
25
30
 
26
31
 
27
32
  module MassiveRecord
@@ -35,6 +40,19 @@ module MassiveRecord
35
40
  # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
36
41
  cattr_accessor :logger, :instance_writer => false
37
42
 
43
+
44
+ #
45
+ # Integers are now persisted as a hax representation in hbase, not as
46
+ # a string any more. This makes for instance atomic_increment!(:int_attr) work
47
+ # as expected.
48
+ #
49
+ # The problem is that if you have old data in your database, you need to handle
50
+ # this. Every new integer will still be written as hex though.
51
+ #
52
+ cattr_accessor :backward_compatibility_integers_might_be_persisted_as_strings, :instance_writer => false
53
+ self.backward_compatibility_integers_might_be_persisted_as_strings = false
54
+
55
+
38
56
  # Add a prefix or a suffix to the table name
39
57
  # example:
40
58
  #
@@ -47,6 +65,18 @@ module MassiveRecord
47
65
 
48
66
  class_attribute :table_name_suffix, :instance_writer => false
49
67
  self.table_name_suffix = ""
68
+
69
+ #
70
+ # Will do a simple exists?(id) check before create as a simple (and
71
+ # kinda insecure) sanity check on if that ID exists or not. If it do
72
+ # exists a RecordNotUnique will be raised. This is done from the ORM
73
+ # layer, so obviously there is a speed cost on create.
74
+ #
75
+ class_attribute :check_record_uniqueness_on_create, :instance_writer => false
76
+ self.check_record_uniqueness_on_create = false
77
+
78
+ class_attribute :auto_increment_id, :instance_writer => false
79
+ self.auto_increment_id = true
50
80
 
51
81
  class << self
52
82
  def table_name
@@ -104,15 +134,20 @@ module MassiveRecord
104
134
  # and assign to instance variables. How read- and write
105
135
  # methods are defined might change over time when the DSL
106
136
  # for describing column families and fields are in place
137
+ # You can call initialize in multiple ways:
138
+ # ORMClass.new(attr_one: value, attr_two: value)
139
+ # ORMClass.new("the-id-of-the-new-record")
140
+ # ORMClass.new("the-id-of-the-new-record", attr_one: value, attr_two: value)
107
141
  #
108
- def initialize(attributes = {})
142
+ def initialize(*args)
143
+ attributes = args.extract_options!
144
+ id = args.first
145
+
109
146
  @new_record = true
110
147
  @destroyed = @readonly = false
111
148
  @relation_proxy_cache = {}
112
149
 
113
- attributes = {} if attributes.nil?
114
-
115
- self.attributes_raw = attributes_from_field_definition
150
+ self.attributes_raw = attributes_from_field_definition.merge('id' => id)
116
151
  self.attributes = attributes
117
152
 
118
153
  clear_dirty_states!
@@ -186,6 +221,11 @@ module MassiveRecord
186
221
  read_attribute(:id)
187
222
  end
188
223
 
224
+ def id=(id)
225
+ id = id.to_s unless id.blank?
226
+ write_attribute(:id, id)
227
+ end
228
+
189
229
 
190
230
 
191
231
  def readonly?
@@ -238,17 +278,20 @@ module MassiveRecord
238
278
 
239
279
  Base.class_eval do
240
280
  include Config
241
- include Relations::Interface
242
281
  include Persistence
282
+ include Relations::Interface
243
283
  include Finders
244
- include ActiveModel::Translation
284
+ extend RescueMissingTableOnFind
245
285
  include AttributeMethods
246
286
  include AttributeMethods::Write, AttributeMethods::Read
287
+ include AttributeMethods::TimeZoneConversion
247
288
  include AttributeMethods::Dirty
248
289
  include Validations
249
290
  include Callbacks
250
291
  include Timestamps
251
292
  include SingleTableInheritance
293
+ include DefaultId
294
+ include QueryInstrumentation
252
295
 
253
296
 
254
297
  alias [] read_attribute
@@ -257,10 +300,9 @@ module MassiveRecord
257
300
  end
258
301
  end
259
302
 
260
-
261
-
262
-
263
-
264
303
  require 'massive_record/orm/table'
265
304
  require 'massive_record/orm/column'
266
305
  require 'massive_record/orm/id_factory'
306
+ require 'massive_record/orm/log_subscriber'
307
+
308
+ ActiveSupport.run_load_hooks(:massive_record, MassiveRecord::ORM::Base)
@@ -44,7 +44,7 @@ module MassiveRecord
44
44
  _run_create_callbacks { super }
45
45
  end
46
46
 
47
- def update
47
+ def update(*)
48
48
  _run_update_callbacks { super }
49
49
  end
50
50
  end
@@ -0,0 +1,20 @@
1
+ module MassiveRecord
2
+ module ORM
3
+ module DefaultId
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_create :ensure_record_has_id, :if => :auto_increment_id
8
+ end
9
+
10
+
11
+ module InstanceMethods
12
+ private
13
+
14
+ def ensure_record_has_id
15
+ self.id = next_id if id.blank?
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -20,6 +20,10 @@ module MassiveRecord
20
20
  class RecordNotFound < MassiveRecordError
21
21
  end
22
22
 
23
+ # Raised when we try to create a new record with an id which exists.
24
+ class RecordNotUnique < MassiveRecordError
25
+ end
26
+
23
27
  # Raised if an attribute is unkown
24
28
  class UnknownAttributeError < MassiveRecordError
25
29
  end
@@ -12,6 +12,79 @@ module MassiveRecord
12
12
  end
13
13
 
14
14
  module ClassMethods
15
+ #
16
+ # Find records in batches. Makes it easier to work with
17
+ # big data sets where you don't want to load every record up front.
18
+ #
19
+ def find_in_batches(*args)
20
+ table.find_in_batches(*args) do |rows|
21
+ records = rows.collect do |row|
22
+ instantiate(transpose_hbase_columns_to_record_attributes(row))
23
+ end
24
+ yield records
25
+ end
26
+ end
27
+
28
+ #
29
+ # Similar to all, except that this will use find_in_batches
30
+ # behind the scene.
31
+ #
32
+ def find_each(*args)
33
+ find_in_batches(*args) do |rows|
34
+ rows.each do |row|
35
+ yield row
36
+ end
37
+ end
38
+ end
39
+
40
+
41
+ #
42
+ # Returns true if a record do exist
43
+ #
44
+ def exists?(id)
45
+ !!find(id) rescue false
46
+ end
47
+
48
+
49
+
50
+ #
51
+ # Entry point for method delegation like find, first, all etc.
52
+ #
53
+ def finder_scope
54
+ default_scoping || unscoped
55
+ end
56
+
57
+
58
+ #
59
+ # Sets a default scope which will be used for calls like find, first, all etc.
60
+ # Makes it possible to for instance set default column families to load on all
61
+ # calls to the database.
62
+ #
63
+ def default_scope(scope)
64
+ self.default_scoping = case scope
65
+ when Scope, nil
66
+ scope
67
+ when Hash
68
+ Scope.new(self, :find_options => scope)
69
+ else
70
+ raise "Don't know how to set scope with #{scope.class}."
71
+ end
72
+ end
73
+
74
+ #
75
+ # Returns an fresh scope object with no limitations set by
76
+ # for instance the default scope
77
+ #
78
+ def unscoped
79
+ Scope.new(self)
80
+ end
81
+
82
+
83
+
84
+
85
+ #
86
+ # This do_find method is not very nice it's logic should be re-factored at some point.
87
+ #
15
88
  def do_find(*args) # :nodoc:
16
89
  options = args.extract_options!.to_options
17
90
  raise ArgumentError.new("At least one argument required!") if args.empty?
@@ -25,26 +98,10 @@ module MassiveRecord
25
98
  type = args.shift if args.first.is_a? Symbol
26
99
  find_many = type == :all
27
100
  expected_result_size = nil
28
-
29
- return (find_many ? [] : raise(RecordNotFound.new("Could not find #{model_name} with id=#{args.first}"))) unless table.exists?
101
+ what_to_find = []
102
+ result_from_table = []
30
103
 
31
- result_from_table = if type
32
- table.send(type, *args) # first() / all()
33
- else
34
- options = args.extract_options!
35
- what_to_find = args.first
36
- expected_result_size = 1
37
-
38
- if args.first.kind_of?(Array)
39
- find_many = true
40
- elsif args.length > 1
41
- find_many = true
42
- what_to_find = args
43
- end
44
-
45
- expected_result_size = what_to_find.length if what_to_find.is_a? Array
46
- table.find(what_to_find, options)
47
- end
104
+ find_many, expected_result_size, what_to_find, result_from_table = query_hbase(type, args, find_many)
48
105
 
49
106
  # Filter out unexpected IDs (unless type is set (all/first), in that case
50
107
  # we have no expectations on the returned rows' ids)
@@ -71,54 +128,42 @@ module MassiveRecord
71
128
  find_many ? records : records.first
72
129
  end
73
130
 
74
- def find_in_batches(*args)
75
- return unless table.exists?
76
131
 
77
- table.find_in_batches(*args) do |rows|
78
- records = rows.collect do |row|
79
- instantiate(transpose_hbase_columns_to_record_attributes(row))
80
- end
81
- yield records
82
- end
83
- end
84
-
85
- def find_each(*args)
86
- find_in_batches(*args) do |rows|
87
- rows.each do |row|
88
- yield row
89
- end
90
- end
91
- end
92
132
 
93
133
 
94
- def exists?(id)
95
- !!find(id) rescue false
96
- end
134
+ private
97
135
 
136
+ def query_hbase(type, args, find_many) # :nodoc:
137
+ result_from_table = if type
138
+ hbase_query_all_first(type, args)
139
+ else
140
+ options = args.extract_options!
141
+ what_to_find = args.first
142
+ expected_result_size = 1
98
143
 
99
- def finder_scope
100
- default_scoping || unscoped
101
- end
144
+ if args.first.kind_of?(Array)
145
+ find_many = true
146
+ elsif args.length > 1
147
+ find_many = true
148
+ what_to_find = args
149
+ end
102
150
 
103
- def default_scope(scope)
104
- self.default_scoping = case scope
105
- when Scope, nil
106
- scope
107
- when Hash
108
- Scope.new(self, :find_options => scope)
109
- else
110
- raise "Don't know how to set scope with #{scope.class}."
111
- end
112
- end
151
+ expected_result_size = what_to_find.length if what_to_find.is_a? Array
152
+ hbase_query_find(what_to_find, options)
153
+ end
113
154
 
114
- def unscoped
115
- Scope.new(self)
155
+ [find_many, expected_result_size, what_to_find, result_from_table]
116
156
  end
117
157
 
158
+ def hbase_query_all_first(type, args)
159
+ table.send(type, *args) # first() / all()
160
+ end
118
161
 
119
- private
162
+ def hbase_query_find(what_to_find, options)
163
+ table.find(what_to_find, options)
164
+ end
120
165
 
121
- def transpose_hbase_columns_to_record_attributes(row)
166
+ def transpose_hbase_columns_to_record_attributes(row) #: nodoc:
122
167
  attributes = {:id => row.id}
123
168
 
124
169
  autoload_column_families_and_fields_with(row.columns.keys)
@@ -131,7 +176,7 @@ module MassiveRecord
131
176
  attributes
132
177
  end
133
178
 
134
- def instantiate(record)
179
+ def instantiate(record) # :nodoc:
135
180
  model = if klass = record[inheritance_attribute] and klass.present?
136
181
  klass.constantize.allocate
137
182
  else
@@ -0,0 +1,45 @@
1
+ module MassiveRecord
2
+ module ORM
3
+ #
4
+ # Module which adds functionality so we rescue errors which might occur on
5
+ # find calls when we are querying tables which does not exist.
6
+ # Small problem with this, which will need to look into.
7
+ #
8
+ module RescueMissingTableOnFind
9
+ def do_find(*args)
10
+ create_table_and_retry_if_table_missing { super }
11
+ end
12
+
13
+ def find_in_batches(*args)
14
+ create_table_and_retry_if_table_missing { super }
15
+ end
16
+
17
+
18
+
19
+ private
20
+
21
+
22
+ #
23
+ # Yields the block and if any errors occur we will check if table does exist or not.
24
+ # Create it if it's missing and try again.
25
+ #
26
+ # Errors which we'll retry on are:
27
+ # Apache::Hadoop::Hbase::Thrift::IOError -> Raised on simple find(id) calls
28
+ # Apache::Hadoop::Hbase::Thrift::IllegalArgument -> Raised when a scanner is used
29
+ #
30
+ def create_table_and_retry_if_table_missing # :nodoc:
31
+ begin
32
+ yield
33
+ rescue Apache::Hadoop::Hbase::Thrift::IOError, Apache::Hadoop::Hbase::Thrift::IllegalArgument => error
34
+ if table.exists?
35
+ raise error
36
+ else
37
+ logger.try :info, "*** TABLE MISSING: Table '#{table_name}' seems to be missing. Will create it, then retry call to find()."
38
+ hbase_create_table!
39
+ yield
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -38,7 +38,7 @@ module MassiveRecord
38
38
  if table_exists?
39
39
  begin
40
40
  if @instance
41
- @instance.reload
41
+ @instance.reload # If, for some reason, the record has been removed. Will be rescued and set to nil
42
42
  else
43
43
  @instance = find(ID)
44
44
  end