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,55 @@
|
|
1
|
+
module MassiveRecord
|
2
|
+
module Rails
|
3
|
+
#
|
4
|
+
# Included into action controller to play nice with
|
5
|
+
# actionpack/lib/action_controller/metal/instrumentation
|
6
|
+
#
|
7
|
+
# log_process_action is the real heart of this module; injecting
|
8
|
+
# the time it took for database queries to complete.
|
9
|
+
#
|
10
|
+
module ControllerRuntime
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
attr_internal :db_runtime
|
18
|
+
|
19
|
+
def process_action(action, *args)
|
20
|
+
# We also need to reset the runtime before each action
|
21
|
+
# because of queries in middleware or in cases we are streaming
|
22
|
+
# and it won't be cleaned up by the method below.
|
23
|
+
MassiveRecord::ORM::LogSubscriber.reset_runtime
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def cleanup_view_runtime
|
29
|
+
db_rt_before_render = MassiveRecord::ORM::LogSubscriber.reset_runtime
|
30
|
+
runtime = super
|
31
|
+
db_rt_after_render = MassiveRecord::ORM::LogSubscriber.reset_runtime
|
32
|
+
self.db_runtime = db_rt_before_render + db_rt_after_render
|
33
|
+
runtime - db_rt_after_render
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
def append_info_to_payload(payload)
|
39
|
+
super
|
40
|
+
payload[:db_runtime] = db_runtime
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def log_process_action(payload)
|
48
|
+
messages, db_runtime = super, payload[:db_runtime]
|
49
|
+
messages << ("MassiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
|
50
|
+
messages
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -5,11 +5,27 @@ module MassiveRecord
|
|
5
5
|
MassiveRecord::ORM::Base.logger = ::Rails.logger
|
6
6
|
end
|
7
7
|
|
8
|
+
# Make Rails handle RecordNotFound correctly in production
|
8
9
|
initializer "massive_record.action_dispatch" do
|
9
10
|
ActiveSupport.on_load :action_controller do
|
10
11
|
ActionDispatch::ShowExceptions.rescue_responses.update('MassiveRecord::ORM::RecordNotFound' => :not_found)
|
11
12
|
end
|
12
13
|
end
|
14
|
+
|
15
|
+
# Expose database runtime to controller for logging.
|
16
|
+
initializer "massive_record.log_runtime" do
|
17
|
+
require "massive_record/rails/controller_runtime"
|
18
|
+
ActiveSupport.on_load(:action_controller) do |app|
|
19
|
+
include MassiveRecord::Rails::ControllerRuntime
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
initializer "massive_record.time_zone_awareness" do
|
24
|
+
ActiveSupport.on_load(:massive_record) do
|
25
|
+
self.time_zone_aware_attributes = true
|
26
|
+
self.default_timezone = :utc
|
27
|
+
end
|
28
|
+
end
|
13
29
|
end
|
14
30
|
end
|
15
31
|
end
|
@@ -1,21 +1,50 @@
|
|
1
1
|
module MassiveRecord
|
2
2
|
module Wrapper
|
3
3
|
class Cell
|
4
|
+
SUPPORTED_TYPES = [NilClass, String, Fixnum, Bignum]
|
5
|
+
|
4
6
|
attr_reader :value
|
5
7
|
attr_accessor :created_at
|
6
8
|
|
9
|
+
|
10
|
+
#
|
11
|
+
# Packs an integer as a 64-bit signed integer, native endian (int64_t)
|
12
|
+
# Reverse it as the byte order in hbase are reversed
|
13
|
+
#
|
14
|
+
def self.integer_to_hex_string(int)
|
15
|
+
[int].pack('q').reverse
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Unpacks an string as a 64-bit signed integer, native endian (int64_t)
|
20
|
+
# Reverse it before unpack as the byte order in hbase are reversed
|
21
|
+
#
|
22
|
+
def self.hex_string_to_integer(string)
|
23
|
+
string.reverse.unpack("q*").first
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
|
7
28
|
def initialize(opts = {})
|
8
29
|
self.value = opts[:value]
|
9
30
|
self.created_at = opts[:created_at]
|
10
31
|
end
|
11
32
|
|
12
33
|
def value=(v)
|
13
|
-
raise "#{v} was a #{v.class}, but it must be a
|
14
|
-
|
34
|
+
raise "#{v} was a #{v.class}, but it must be a one of: #{SUPPORTED_TYPES.join(', ')}" unless SUPPORTED_TYPES.include? v.class
|
35
|
+
|
36
|
+
@value = v.duplicable? ? v.dup : v
|
15
37
|
end
|
16
38
|
|
17
39
|
def value_to_thrift
|
18
|
-
value
|
40
|
+
case value
|
41
|
+
when String
|
42
|
+
value.force_encoding(Encoding::BINARY)
|
43
|
+
when Fixnum, Bignum
|
44
|
+
self.class.integer_to_hex_string(value)
|
45
|
+
when NilClass
|
46
|
+
value
|
47
|
+
end
|
19
48
|
end
|
20
49
|
end
|
21
50
|
end
|
data/massive_record.gemspec
CHANGED
File without changes
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MassiveRecord::Wrapper::Row do
|
4
|
+
before :all do
|
5
|
+
@connection = MassiveRecord::Wrapper::Connection.new(:host => MR_CONFIG['host'], :port => MR_CONFIG['port'])
|
6
|
+
@connection.open
|
7
|
+
@table = MassiveRecord::Wrapper::Table.new(@connection, MR_CONFIG['table']).tap do |table|
|
8
|
+
table.column_families.create(:misc)
|
9
|
+
table.save
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
after do
|
14
|
+
@table.all.each &:destroy
|
15
|
+
end
|
16
|
+
|
17
|
+
after :all do
|
18
|
+
@table.destroy
|
19
|
+
@connection.close
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
let(:atomic_inc_attr_name) { 'misc:atomic' }
|
24
|
+
subject do
|
25
|
+
MassiveRecord::Wrapper::Row.new.tap do |row|
|
26
|
+
row.id = "ID1"
|
27
|
+
row.table = @table
|
28
|
+
row.save
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
describe "#atomic_increment" do
|
36
|
+
it "increments to 1 when called on a new value" do
|
37
|
+
subject.atomic_increment(atomic_inc_attr_name).should eq 1
|
38
|
+
end
|
39
|
+
|
40
|
+
it "increments by 2 when asked to do so" do
|
41
|
+
subject.atomic_increment(atomic_inc_attr_name, 2).should eq 2
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#read_atomic_integer_value" do
|
46
|
+
it "returns 0 if no atomic increment operation has been performed" do
|
47
|
+
subject.read_atomic_integer_value(atomic_inc_attr_name).should eq 0
|
48
|
+
end
|
49
|
+
|
50
|
+
it "returns 1 after one incrementation of 1" do
|
51
|
+
subject.atomic_increment(atomic_inc_attr_name)
|
52
|
+
subject.read_atomic_integer_value(atomic_inc_attr_name).should eq 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -43,14 +43,4 @@ describe "A connection" do
|
|
43
43
|
@connection.open
|
44
44
|
@connection.tables.should be_a_kind_of(MassiveRecord::Wrapper::TablesCollection)
|
45
45
|
end
|
46
|
-
|
47
|
-
it "should reconnect on IOError" do
|
48
|
-
@connection.open
|
49
|
-
@connection.transport.open?.should be_true
|
50
|
-
@connection.getTableNames().should be_a_kind_of(Array)
|
51
|
-
|
52
|
-
@connection.close
|
53
|
-
@connection.transport.open?.should be_false
|
54
|
-
@connection.getTableNames().should be_a_kind_of(Array)
|
55
|
-
end
|
56
46
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MassiveRecord::Adapters::Thrift::Table do
|
4
|
+
let(:connection) do
|
5
|
+
MassiveRecord::Wrapper::Connection.new(:host => MR_CONFIG['host'], :port => MR_CONFIG['port']).tap do |connection|
|
6
|
+
connection.open
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
subject do
|
11
|
+
MassiveRecord::Wrapper::Table.new(@connection, MR_CONFIG['table'])
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
before :all do
|
16
|
+
subject.column_families.create(:base)
|
17
|
+
subject.save
|
18
|
+
end
|
19
|
+
|
20
|
+
after :all do
|
21
|
+
subject.destroy
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
before do
|
27
|
+
2.times do |index|
|
28
|
+
MassiveRecord::Wrapper::Row.new.tap do |row|
|
29
|
+
row.id = index.to_s
|
30
|
+
row.values = {:base => {:first_name => "John-#{index}", :last_name => "Doe-#{index}" }}
|
31
|
+
row.table = subject
|
32
|
+
row.save
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
after do
|
38
|
+
subject.all.each &:destroy
|
39
|
+
end
|
40
|
+
end
|
@@ -18,6 +18,10 @@ describe "A table" do
|
|
18
18
|
it "should not exists is the database" do
|
19
19
|
@connection.tables.should_not include(MR_CONFIG['table'])
|
20
20
|
end
|
21
|
+
|
22
|
+
it "should not exists in the database" do
|
23
|
+
@table.exists?.should be_false
|
24
|
+
end
|
21
25
|
|
22
26
|
it "should not have any column families" do
|
23
27
|
@table.column_families.should be_empty
|
@@ -54,27 +58,32 @@ describe "A table" do
|
|
54
58
|
row.id = "ID1"
|
55
59
|
row.values = {
|
56
60
|
:info => { :first_name => "John", :last_name => "Doe", :email => "john@base.com" },
|
57
|
-
:misc => {
|
61
|
+
:misc => {
|
62
|
+
:integer => 1234567,
|
63
|
+
:null_test => "some-value",
|
58
64
|
:like => ["Eating", "Sleeping", "Coding"].to_json,
|
59
65
|
:dislike => {
|
60
66
|
"Washing" => "Boring 6/10",
|
61
67
|
"Ironing" => "Boring 8/10"
|
62
68
|
}.to_json,
|
63
|
-
:empty => {}.to_json
|
64
|
-
:value_to_increment => "1"
|
69
|
+
:empty => {}.to_json
|
65
70
|
}
|
66
71
|
}
|
67
72
|
row.table = @table
|
68
73
|
row.save
|
69
74
|
end
|
70
75
|
|
71
|
-
it "should list
|
72
|
-
@table.column_names.size.should ==
|
76
|
+
it "should list all column names" do
|
77
|
+
@table.column_names.size.should == 8
|
73
78
|
end
|
74
79
|
|
75
80
|
it "should only load one column" do
|
76
81
|
@table.get("ID1", :info, :first_name).should == "John"
|
77
82
|
end
|
83
|
+
|
84
|
+
it "should return nil if column does not exist" do
|
85
|
+
@table.get("ID1", :info, :unkown_column).should be_nil
|
86
|
+
end
|
78
87
|
|
79
88
|
it "should only load one column family" do
|
80
89
|
@table.first(:select => ["info"]).column_families.should == ["info"]
|
@@ -82,6 +91,10 @@ describe "A table" do
|
|
82
91
|
@table.find("ID1", :select => ["info"]).column_families.should == ["info"]
|
83
92
|
end
|
84
93
|
|
94
|
+
it "should return nil if id is not found" do
|
95
|
+
@table.find("not_exist_FOO").should be_nil
|
96
|
+
end
|
97
|
+
|
85
98
|
it "should update row values" do
|
86
99
|
row = @table.first
|
87
100
|
row.values["info:first_name"].should eql("John")
|
@@ -92,6 +105,11 @@ describe "A table" do
|
|
92
105
|
row.update_column(:info, :email, "bob@base.com")
|
93
106
|
row.values["info:email"].should eql("bob@base.com")
|
94
107
|
end
|
108
|
+
|
109
|
+
it "should persist integer values as binary" do
|
110
|
+
row = @table.first
|
111
|
+
row.values["misc:integer"].should eq [1234567].pack('q').reverse
|
112
|
+
end
|
95
113
|
|
96
114
|
it "should save row changes" do
|
97
115
|
row = @table.first
|
@@ -140,7 +158,7 @@ describe "A table" do
|
|
140
158
|
row = @table.first
|
141
159
|
row.update_columns({ :misc => { :super_power => "Eating"} })
|
142
160
|
row.columns.collect{|k, v| k if k.include?("misc:")}.delete_if{|v| v.nil?}.sort.should(
|
143
|
-
eql(["misc:
|
161
|
+
eql(["misc:null_test", "misc:integer", "misc:like", "misc:empty", "misc:dislike", "misc:super_power"].sort)
|
144
162
|
)
|
145
163
|
end
|
146
164
|
|
@@ -161,21 +179,37 @@ describe "A table" do
|
|
161
179
|
row.values["misc:genre"].should == "M"
|
162
180
|
end
|
163
181
|
|
164
|
-
it "should be able to do atomic increment call on
|
182
|
+
it "should be able to do atomic increment call on new cell" do
|
165
183
|
row = @table.first
|
166
|
-
row.values["misc:value_to_increment"].should == "1"
|
167
184
|
|
168
185
|
result = row.atomic_increment("misc:value_to_increment")
|
169
|
-
result.should ==
|
186
|
+
result.should == 1
|
170
187
|
end
|
171
188
|
|
172
|
-
it "should be able to pass
|
189
|
+
it "should be able to pass in what to incremet the new cell by" do
|
173
190
|
row = @table.first
|
174
|
-
row.
|
175
|
-
row.atomic_increment("misc:value_to_increment", 2)
|
191
|
+
result = row.atomic_increment("misc:value_to_increment", 2)
|
176
192
|
|
193
|
+
result.should == 3
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should be able to do atomic increment on existing values" do
|
177
197
|
row = @table.first
|
178
|
-
|
198
|
+
|
199
|
+
result = row.atomic_increment("misc:integer")
|
200
|
+
result.should == 1234568
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should be settable to nil" do
|
204
|
+
row = @table.first
|
205
|
+
|
206
|
+
row.values["misc:null_test"].should_not be_nil
|
207
|
+
|
208
|
+
row.update_column(:misc, :null_test, nil)
|
209
|
+
row.save
|
210
|
+
|
211
|
+
row = @table.first
|
212
|
+
row.values["misc:null_test"].should be_nil
|
179
213
|
end
|
180
214
|
|
181
215
|
it "should delete a row" do
|
@@ -189,6 +223,14 @@ describe "A table" do
|
|
189
223
|
it "should exists in the database" do
|
190
224
|
@table.exists?.should be_true
|
191
225
|
end
|
226
|
+
|
227
|
+
it "should only check for existance once" do
|
228
|
+
connection = mock(Object)
|
229
|
+
connection.should_receive(:tables).and_return [MR_CONFIG['table']]
|
230
|
+
@table.should_receive(:connection).and_return(connection)
|
231
|
+
|
232
|
+
2.times { @table.exists? }
|
233
|
+
end
|
192
234
|
|
193
235
|
it "should destroy the test table" do
|
194
236
|
@table.destroy.should be_true
|
@@ -3,7 +3,7 @@ require 'orm/models/person'
|
|
3
3
|
|
4
4
|
describe "attribute methods" do
|
5
5
|
before do
|
6
|
-
@model = Person.new
|
6
|
+
@model = Person.new "5", :name => "John", :age => "15"
|
7
7
|
end
|
8
8
|
|
9
9
|
it "should define reader method" do
|
@@ -27,6 +27,11 @@ describe "attribute methods" do
|
|
27
27
|
it "should return casted value when read" do
|
28
28
|
@model.read_attribute(:age).should == 15
|
29
29
|
end
|
30
|
+
|
31
|
+
it "should read from a method if it has been defined" do
|
32
|
+
@model.should_receive(:_name).and_return("my name is")
|
33
|
+
@model.read_attribute(:name).should eq "my name is"
|
34
|
+
end
|
30
35
|
|
31
36
|
describe "#attributes" do
|
32
37
|
it "should contain the id" do
|