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
@@ -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(
|
8
|
-
new(
|
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
|
-
|
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
|
-
|
94
|
-
|
95
|
-
self[attr_name] =
|
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(
|
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
|
@@ -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
|
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
|
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
|
146
|
+
if value.nil? || should_not_be_encoded?
|
139
147
|
value
|
140
148
|
else
|
141
|
-
|
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
|