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