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