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