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.
Files changed (68) hide show
  1. data/CHANGELOG.md +43 -4
  2. data/Gemfile.lock +3 -1
  3. data/README.md +5 -0
  4. data/lib/massive_record/adapters/thrift/connection.rb +23 -16
  5. data/lib/massive_record/adapters/thrift/row.rb +13 -33
  6. data/lib/massive_record/adapters/thrift/table.rb +24 -10
  7. data/lib/massive_record/orm/attribute_methods.rb +27 -1
  8. data/lib/massive_record/orm/attribute_methods/dirty.rb +2 -2
  9. data/lib/massive_record/orm/attribute_methods/read.rb +36 -1
  10. data/lib/massive_record/orm/attribute_methods/time_zone_conversion.rb +81 -0
  11. data/lib/massive_record/orm/attribute_methods/write.rb +18 -0
  12. data/lib/massive_record/orm/base.rb +52 -10
  13. data/lib/massive_record/orm/callbacks.rb +1 -1
  14. data/lib/massive_record/orm/default_id.rb +20 -0
  15. data/lib/massive_record/orm/errors.rb +4 -0
  16. data/lib/massive_record/orm/finders.rb +102 -57
  17. data/lib/massive_record/orm/finders/rescue_missing_table_on_find.rb +45 -0
  18. data/lib/massive_record/orm/id_factory.rb +1 -1
  19. data/lib/massive_record/orm/log_subscriber.rb +85 -0
  20. data/lib/massive_record/orm/persistence.rb +82 -37
  21. data/lib/massive_record/orm/query_instrumentation.rb +64 -0
  22. data/lib/massive_record/orm/relations/interface.rb +10 -0
  23. data/lib/massive_record/orm/relations/metadata.rb +2 -0
  24. data/lib/massive_record/orm/relations/proxy/references_one_polymorphic.rb +1 -1
  25. data/lib/massive_record/orm/schema/field.rb +33 -6
  26. data/lib/massive_record/orm/timestamps.rb +1 -1
  27. data/lib/massive_record/orm/validations.rb +2 -2
  28. data/lib/massive_record/rails/controller_runtime.rb +55 -0
  29. data/lib/massive_record/rails/railtie.rb +16 -0
  30. data/lib/massive_record/version.rb +1 -1
  31. data/lib/massive_record/wrapper/cell.rb +32 -3
  32. data/massive_record.gemspec +1 -0
  33. data/spec/{wrapper/cases → adapter/thrift}/adapter_spec.rb +0 -0
  34. data/spec/adapter/thrift/atomic_increment_spec.rb +55 -0
  35. data/spec/{wrapper/cases → adapter/thrift}/connection_spec.rb +0 -10
  36. data/spec/adapter/thrift/table_find_spec.rb +40 -0
  37. data/spec/{wrapper/cases → adapter/thrift}/table_spec.rb +55 -13
  38. data/spec/orm/cases/attribute_methods_spec.rb +6 -1
  39. data/spec/orm/cases/base_spec.rb +18 -4
  40. data/spec/orm/cases/callbacks_spec.rb +1 -1
  41. data/spec/orm/cases/default_id_spec.rb +38 -0
  42. data/spec/orm/cases/default_values_spec.rb +37 -0
  43. data/spec/orm/cases/dirty_spec.rb +25 -1
  44. data/spec/orm/cases/encoding_spec.rb +3 -3
  45. data/spec/orm/cases/finder_default_scope.rb +8 -1
  46. data/spec/orm/cases/finder_scope_spec.rb +2 -2
  47. data/spec/orm/cases/finders_spec.rb +8 -18
  48. data/spec/orm/cases/id_factory_spec.rb +38 -21
  49. data/spec/orm/cases/log_subscriber_spec.rb +133 -0
  50. data/spec/orm/cases/mass_assignment_security_spec.rb +97 -0
  51. data/spec/orm/cases/persistence_spec.rb +132 -27
  52. data/spec/orm/cases/single_table_inheritance_spec.rb +2 -2
  53. data/spec/orm/cases/time_zone_awareness_spec.rb +157 -0
  54. data/spec/orm/cases/timestamps_spec.rb +15 -0
  55. data/spec/orm/cases/validation_spec.rb +2 -2
  56. data/spec/orm/models/model_without_default_id.rb +5 -0
  57. data/spec/orm/models/person.rb +1 -0
  58. data/spec/orm/models/test_class.rb +1 -0
  59. data/spec/orm/relations/interface_spec.rb +2 -2
  60. data/spec/orm/relations/metadata_spec.rb +1 -1
  61. data/spec/orm/relations/proxy/references_many_spec.rb +21 -15
  62. data/spec/orm/relations/proxy/references_one_polymorphic_spec.rb +7 -1
  63. data/spec/orm/relations/proxy/references_one_spec.rb +7 -0
  64. data/spec/orm/schema/field_spec.rb +61 -5
  65. data/spec/support/connection_helpers.rb +2 -1
  66. data/spec/support/mock_massive_record_connection.rb +7 -0
  67. data/spec/support/time_zone_helper.rb +25 -0
  68. metadata +51 -14
@@ -28,7 +28,7 @@ module MassiveRecord
28
28
 
29
29
 
30
30
  def updated_at
31
- self['updated_at']
31
+ self.class.time_zone_aware_attributes ? self['updated_at'].try(:in_time_zone) : self['updated_at']
32
32
  end
33
33
 
34
34
  def write_attribute(attr_name, value)
@@ -24,8 +24,8 @@ module MassiveRecord
24
24
 
25
25
 
26
26
  module ClassMethods
27
- def create!(attributes = {})
28
- record = new(attributes)
27
+ def create!(*args)
28
+ record = new(*args)
29
29
  record.save!
30
30
  record
31
31
  end
@@ -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,3 +1,3 @@
1
1
  module MassiveRecord
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1.rc1"
3
3
  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 String!" unless v.is_a? String
14
- @value = v.dup.force_encoding(Encoding::UTF_8)
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.force_encoding(Encoding::BINARY)
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
@@ -17,6 +17,7 @@ Gem::Specification.new do |s|
17
17
  s.add_dependency "thrift", ">= 0.5.0"
18
18
  s.add_dependency "activesupport"
19
19
  s.add_dependency "activemodel"
20
+ s.add_dependency "tzinfo"
20
21
 
21
22
  s.add_development_dependency "rspec", ">= 2.1.0"
22
23
 
@@ -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 5 column names" do
72
- @table.column_names.size.should == 7
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:value_to_increment", "misc:like", "misc:empty", "misc:dislike", "misc:super_power"].sort)
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 values" do
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 == "2"
186
+ result.should == 1
170
187
  end
171
188
 
172
- it "should be able to pass inn what to incremet by" do
189
+ it "should be able to pass in what to incremet the new cell by" do
173
190
  row = @table.first
174
- row.values["misc:value_to_increment"].should == "2"
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
- row.values["misc:value_to_increment"].should == "4"
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 :id => 5, :name => "John", :age => "15"
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