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.
- data/CHANGELOG.md +43 -4
- data/Gemfile.lock +3 -1
- data/README.md +5 -0
- data/lib/massive_record/adapters/thrift/connection.rb +23 -16
- data/lib/massive_record/adapters/thrift/row.rb +13 -33
- data/lib/massive_record/adapters/thrift/table.rb +24 -10
- data/lib/massive_record/orm/attribute_methods.rb +27 -1
- data/lib/massive_record/orm/attribute_methods/dirty.rb +2 -2
- data/lib/massive_record/orm/attribute_methods/read.rb +36 -1
- data/lib/massive_record/orm/attribute_methods/time_zone_conversion.rb +81 -0
- data/lib/massive_record/orm/attribute_methods/write.rb +18 -0
- data/lib/massive_record/orm/base.rb +52 -10
- data/lib/massive_record/orm/callbacks.rb +1 -1
- data/lib/massive_record/orm/default_id.rb +20 -0
- data/lib/massive_record/orm/errors.rb +4 -0
- data/lib/massive_record/orm/finders.rb +102 -57
- data/lib/massive_record/orm/finders/rescue_missing_table_on_find.rb +45 -0
- data/lib/massive_record/orm/id_factory.rb +1 -1
- data/lib/massive_record/orm/log_subscriber.rb +85 -0
- data/lib/massive_record/orm/persistence.rb +82 -37
- data/lib/massive_record/orm/query_instrumentation.rb +64 -0
- data/lib/massive_record/orm/relations/interface.rb +10 -0
- data/lib/massive_record/orm/relations/metadata.rb +2 -0
- data/lib/massive_record/orm/relations/proxy/references_one_polymorphic.rb +1 -1
- data/lib/massive_record/orm/schema/field.rb +33 -6
- data/lib/massive_record/orm/timestamps.rb +1 -1
- data/lib/massive_record/orm/validations.rb +2 -2
- data/lib/massive_record/rails/controller_runtime.rb +55 -0
- data/lib/massive_record/rails/railtie.rb +16 -0
- data/lib/massive_record/version.rb +1 -1
- data/lib/massive_record/wrapper/cell.rb +32 -3
- data/massive_record.gemspec +1 -0
- data/spec/{wrapper/cases → adapter/thrift}/adapter_spec.rb +0 -0
- data/spec/adapter/thrift/atomic_increment_spec.rb +55 -0
- data/spec/{wrapper/cases → adapter/thrift}/connection_spec.rb +0 -10
- data/spec/adapter/thrift/table_find_spec.rb +40 -0
- data/spec/{wrapper/cases → adapter/thrift}/table_spec.rb +55 -13
- data/spec/orm/cases/attribute_methods_spec.rb +6 -1
- data/spec/orm/cases/base_spec.rb +18 -4
- data/spec/orm/cases/callbacks_spec.rb +1 -1
- data/spec/orm/cases/default_id_spec.rb +38 -0
- data/spec/orm/cases/default_values_spec.rb +37 -0
- data/spec/orm/cases/dirty_spec.rb +25 -1
- data/spec/orm/cases/encoding_spec.rb +3 -3
- data/spec/orm/cases/finder_default_scope.rb +8 -1
- data/spec/orm/cases/finder_scope_spec.rb +2 -2
- data/spec/orm/cases/finders_spec.rb +8 -18
- data/spec/orm/cases/id_factory_spec.rb +38 -21
- data/spec/orm/cases/log_subscriber_spec.rb +133 -0
- data/spec/orm/cases/mass_assignment_security_spec.rb +97 -0
- data/spec/orm/cases/persistence_spec.rb +132 -27
- data/spec/orm/cases/single_table_inheritance_spec.rb +2 -2
- data/spec/orm/cases/time_zone_awareness_spec.rb +157 -0
- data/spec/orm/cases/timestamps_spec.rb +15 -0
- data/spec/orm/cases/validation_spec.rb +2 -2
- data/spec/orm/models/model_without_default_id.rb +5 -0
- data/spec/orm/models/person.rb +1 -0
- data/spec/orm/models/test_class.rb +1 -0
- data/spec/orm/relations/interface_spec.rb +2 -2
- data/spec/orm/relations/metadata_spec.rb +1 -1
- data/spec/orm/relations/proxy/references_many_spec.rb +21 -15
- data/spec/orm/relations/proxy/references_one_polymorphic_spec.rb +7 -1
- data/spec/orm/relations/proxy/references_one_spec.rb +7 -0
- data/spec/orm/schema/field_spec.rb +61 -5
- data/spec/support/connection_helpers.rb +2 -1
- data/spec/support/mock_massive_record_connection.rb +7 -0
- data/spec/support/time_zone_helper.rb +25 -0
- 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(
|
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
|
-
|
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
|
-
|
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)
|
@@ -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
|
-
|
101
|
+
what_to_find = []
|
102
|
+
result_from_table = []
|
30
103
|
|
31
|
-
result_from_table =
|
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
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
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
|
-
|
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
|