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