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
@@ -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