dynamoid-moda 0.7.1

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 (136) hide show
  1. checksums.yaml +15 -0
  2. data/.document +5 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +24 -0
  6. data/Gemfile.lock +118 -0
  7. data/Gemfile_activemodel4 +24 -0
  8. data/Gemfile_activemodel4.lock +88 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.markdown +360 -0
  11. data/Rakefile +93 -0
  12. data/VERSION +1 -0
  13. data/doc/.nojekyll +0 -0
  14. data/doc/Dynamoid.html +328 -0
  15. data/doc/Dynamoid/Adapter.html +1872 -0
  16. data/doc/Dynamoid/Adapter/AwsSdk.html +2101 -0
  17. data/doc/Dynamoid/Adapter/Local.html +1574 -0
  18. data/doc/Dynamoid/Associations.html +138 -0
  19. data/doc/Dynamoid/Associations/Association.html +847 -0
  20. data/doc/Dynamoid/Associations/BelongsTo.html +161 -0
  21. data/doc/Dynamoid/Associations/ClassMethods.html +766 -0
  22. data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +167 -0
  23. data/doc/Dynamoid/Associations/HasMany.html +167 -0
  24. data/doc/Dynamoid/Associations/HasOne.html +161 -0
  25. data/doc/Dynamoid/Associations/ManyAssociation.html +1684 -0
  26. data/doc/Dynamoid/Associations/SingleAssociation.html +627 -0
  27. data/doc/Dynamoid/Components.html +242 -0
  28. data/doc/Dynamoid/Config.html +412 -0
  29. data/doc/Dynamoid/Config/Options.html +638 -0
  30. data/doc/Dynamoid/Criteria.html +138 -0
  31. data/doc/Dynamoid/Criteria/Chain.html +1471 -0
  32. data/doc/Dynamoid/Criteria/ClassMethods.html +105 -0
  33. data/doc/Dynamoid/Dirty.html +424 -0
  34. data/doc/Dynamoid/Dirty/ClassMethods.html +174 -0
  35. data/doc/Dynamoid/Document.html +1033 -0
  36. data/doc/Dynamoid/Document/ClassMethods.html +1116 -0
  37. data/doc/Dynamoid/Errors.html +125 -0
  38. data/doc/Dynamoid/Errors/ConditionalCheckFailedException.html +141 -0
  39. data/doc/Dynamoid/Errors/DocumentNotValid.html +221 -0
  40. data/doc/Dynamoid/Errors/Error.html +137 -0
  41. data/doc/Dynamoid/Errors/InvalidField.html +141 -0
  42. data/doc/Dynamoid/Errors/InvalidQuery.html +131 -0
  43. data/doc/Dynamoid/Errors/MissingRangeKey.html +141 -0
  44. data/doc/Dynamoid/Fields.html +686 -0
  45. data/doc/Dynamoid/Fields/ClassMethods.html +438 -0
  46. data/doc/Dynamoid/Finders.html +135 -0
  47. data/doc/Dynamoid/Finders/ClassMethods.html +943 -0
  48. data/doc/Dynamoid/IdentityMap.html +492 -0
  49. data/doc/Dynamoid/IdentityMap/ClassMethods.html +534 -0
  50. data/doc/Dynamoid/Indexes.html +321 -0
  51. data/doc/Dynamoid/Indexes/ClassMethods.html +369 -0
  52. data/doc/Dynamoid/Indexes/Index.html +1142 -0
  53. data/doc/Dynamoid/Middleware.html +115 -0
  54. data/doc/Dynamoid/Middleware/IdentityMap.html +264 -0
  55. data/doc/Dynamoid/Persistence.html +892 -0
  56. data/doc/Dynamoid/Persistence/ClassMethods.html +836 -0
  57. data/doc/Dynamoid/Validations.html +415 -0
  58. data/doc/_index.html +506 -0
  59. data/doc/class_list.html +53 -0
  60. data/doc/css/common.css +1 -0
  61. data/doc/css/full_list.css +57 -0
  62. data/doc/css/style.css +338 -0
  63. data/doc/file.LICENSE.html +73 -0
  64. data/doc/file.README.html +416 -0
  65. data/doc/file_list.html +58 -0
  66. data/doc/frames.html +28 -0
  67. data/doc/index.html +416 -0
  68. data/doc/js/app.js +214 -0
  69. data/doc/js/full_list.js +178 -0
  70. data/doc/js/jquery.js +4 -0
  71. data/doc/method_list.html +1144 -0
  72. data/doc/top-level-namespace.html +112 -0
  73. data/dynamoid-moda.gemspec +210 -0
  74. data/dynamoid.gemspec +208 -0
  75. data/lib/dynamoid.rb +46 -0
  76. data/lib/dynamoid/adapter.rb +267 -0
  77. data/lib/dynamoid/adapter/aws_sdk.rb +309 -0
  78. data/lib/dynamoid/associations.rb +106 -0
  79. data/lib/dynamoid/associations/association.rb +105 -0
  80. data/lib/dynamoid/associations/belongs_to.rb +44 -0
  81. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
  82. data/lib/dynamoid/associations/has_many.rb +39 -0
  83. data/lib/dynamoid/associations/has_one.rb +39 -0
  84. data/lib/dynamoid/associations/many_association.rb +191 -0
  85. data/lib/dynamoid/associations/single_association.rb +69 -0
  86. data/lib/dynamoid/components.rb +37 -0
  87. data/lib/dynamoid/config.rb +57 -0
  88. data/lib/dynamoid/config/options.rb +78 -0
  89. data/lib/dynamoid/criteria.rb +29 -0
  90. data/lib/dynamoid/criteria/chain.rb +326 -0
  91. data/lib/dynamoid/dirty.rb +47 -0
  92. data/lib/dynamoid/document.rb +199 -0
  93. data/lib/dynamoid/errors.rb +28 -0
  94. data/lib/dynamoid/fields.rb +138 -0
  95. data/lib/dynamoid/finders.rb +133 -0
  96. data/lib/dynamoid/identity_map.rb +96 -0
  97. data/lib/dynamoid/indexes.rb +69 -0
  98. data/lib/dynamoid/indexes/index.rb +103 -0
  99. data/lib/dynamoid/middleware/identity_map.rb +16 -0
  100. data/lib/dynamoid/persistence.rb +292 -0
  101. data/lib/dynamoid/validations.rb +36 -0
  102. data/spec/app/models/address.rb +13 -0
  103. data/spec/app/models/camel_case.rb +34 -0
  104. data/spec/app/models/car.rb +6 -0
  105. data/spec/app/models/magazine.rb +11 -0
  106. data/spec/app/models/message.rb +9 -0
  107. data/spec/app/models/nuclear_submarine.rb +5 -0
  108. data/spec/app/models/sponsor.rb +8 -0
  109. data/spec/app/models/subscription.rb +12 -0
  110. data/spec/app/models/tweet.rb +12 -0
  111. data/spec/app/models/user.rb +26 -0
  112. data/spec/app/models/vehicle.rb +7 -0
  113. data/spec/dynamoid/adapter/aws_sdk_spec.rb +376 -0
  114. data/spec/dynamoid/adapter_spec.rb +155 -0
  115. data/spec/dynamoid/associations/association_spec.rb +194 -0
  116. data/spec/dynamoid/associations/belongs_to_spec.rb +71 -0
  117. data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +47 -0
  118. data/spec/dynamoid/associations/has_many_spec.rb +42 -0
  119. data/spec/dynamoid/associations/has_one_spec.rb +45 -0
  120. data/spec/dynamoid/associations_spec.rb +16 -0
  121. data/spec/dynamoid/config_spec.rb +27 -0
  122. data/spec/dynamoid/criteria/chain_spec.rb +210 -0
  123. data/spec/dynamoid/criteria_spec.rb +75 -0
  124. data/spec/dynamoid/dirty_spec.rb +57 -0
  125. data/spec/dynamoid/document_spec.rb +180 -0
  126. data/spec/dynamoid/fields_spec.rb +156 -0
  127. data/spec/dynamoid/finders_spec.rb +147 -0
  128. data/spec/dynamoid/identity_map_spec.rb +45 -0
  129. data/spec/dynamoid/indexes/index_spec.rb +104 -0
  130. data/spec/dynamoid/indexes_spec.rb +25 -0
  131. data/spec/dynamoid/persistence_spec.rb +301 -0
  132. data/spec/dynamoid/validations_spec.rb +36 -0
  133. data/spec/dynamoid_spec.rb +14 -0
  134. data/spec/spec_helper.rb +55 -0
  135. data/spec/support/with_partitioning.rb +15 -0
  136. metadata +363 -0
@@ -0,0 +1,45 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "Dynamoid::IdentityMap" do
4
+ before :each do
5
+ Dynamoid::Config.identity_map = true
6
+ end
7
+
8
+ after :each do
9
+ Dynamoid::Config.identity_map = false
10
+ end
11
+
12
+ context 'object identity' do
13
+ it 'maintains a single object' do
14
+ tweet = Tweet.create(:tweet_id => "x", :group => "one")
15
+ tweet1 = Tweet.where(:tweet_id => "x", :group => "one").first
16
+ tweet.should equal(tweet1)
17
+ end
18
+ end
19
+
20
+ context 'cache' do
21
+ it 'uses cache' do
22
+ tweet = Tweet.create(:tweet_id => "x", :group => "one")
23
+ Dynamoid::Adapter.expects(:read).never
24
+ tweet1 = Tweet.find_by_id("x", :range_key => "one")
25
+ tweet.should equal(tweet1)
26
+ end
27
+
28
+ it 'clears cache on delete' do
29
+ tweet = Tweet.create(:tweet_id => "x", :group => "one")
30
+ tweet.delete
31
+ Tweet.find_by_id("x", :range_key => "one").should be_nil
32
+ end
33
+ end
34
+
35
+ context 'clear' do
36
+ it 'clears the identiy map' do
37
+ Tweet.create(:tweet_id => "x", :group => "one")
38
+ Tweet.create(:tweet_id => "x", :group => "two")
39
+
40
+ Tweet.identity_map.size.should eq(2)
41
+ Dynamoid::IdentityMap.clear
42
+ Tweet.identity_map.size.should eq(0)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,104 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe "Dynamoid::Indexes::Index" do
4
+
5
+ before do
6
+ @time = DateTime.now
7
+ @index = Dynamoid::Indexes::Index.new(User, [:password, :name], :range_key => :created_at)
8
+ end
9
+
10
+ it 'reorders keys alphabetically' do
11
+ @index.sort([:password, :name, :created_at]).should == [:created_at, :name, :password]
12
+ end
13
+
14
+ it 'assigns itself hash keys' do
15
+ @index.hash_keys.should == [:name, :password]
16
+ end
17
+
18
+ it 'assigns itself range keys' do
19
+ @index.range_keys.should == [:created_at]
20
+ end
21
+
22
+ it 'reorders its name automatically' do
23
+ @index.name.should == [:created_at, :name, :password]
24
+ end
25
+
26
+ it 'determines its own table name' do
27
+ @index.table_name.should == 'dynamoid_tests_index_user_created_ats_and_names_and_passwords'
28
+ end
29
+
30
+ it 'raises an error if a field does not exist' do
31
+ lambda {@index = Dynamoid::Indexes::Index.new(User, [:password, :text])}.should raise_error(Dynamoid::Errors::InvalidField)
32
+ end
33
+
34
+ it 'returns values for indexes' do
35
+ @index.values(:name => 'Josh', :password => 'test123', :created_at => @time).should == {:hash_value => 'Josh.test123', :range_value => @time.to_f}
36
+ end
37
+
38
+ it 'ignores values for fields that do not exist' do
39
+ @index.values(:name => 'Josh', :password => 'test123', :created_at => @time, :email => 'josh@joshsymonds.com').should == {:hash_value => 'Josh.test123', :range_value => @time.to_f}
40
+ end
41
+
42
+ it 'substitutes a blank string for a hash value that does not exist' do
43
+ @index.values(:name => 'Josh', :created_at => @time).should == {:hash_value => 'Josh.', :range_value => @time.to_f}
44
+ end
45
+
46
+ it 'substitutes a blank string if both hash values do not exist' do
47
+ @index.values(:created_at => @time).should == {:hash_value => '.', :range_value => @time.to_f}
48
+ end
49
+
50
+ it 'accepts values from an object instead of a hash' do
51
+ @user = User.new(:name => 'Josh', :password => 'test123', :created_at => @time)
52
+
53
+ @index.values(@user).should == {:hash_value => 'Josh.test123', :range_value => @time.to_f}
54
+ end
55
+
56
+ it 'accepts values from an object and returns changed values' do
57
+ @user = User.new(:name => 'Josh', :password => 'test123', :created_at => @time)
58
+ @user.name = 'Justin'
59
+
60
+ @index.values(@user).should == {:hash_value => 'Justin.test123', :range_value => @time.to_f}
61
+ end
62
+
63
+ it 'saves an object to the index it is associated with' do
64
+ @index = Dynamoid::Indexes::Index.new(User, :name)
65
+ @user = User.new(:name => 'Josh', :password => 'test123', :created_at => @time, :id => 'test123')
66
+
67
+ @index.save(@user)
68
+
69
+ Dynamoid::Adapter.read("dynamoid_tests_index_user_names", 'Josh')[:ids].should == Set['test123']
70
+ end
71
+
72
+ it 'saves an object to the index it is associated with with a range' do
73
+ @index = Dynamoid::Indexes::Index.new(User, :name, :range_key => :last_logged_in_at)
74
+ @user = User.create(:name => 'Josh', :last_logged_in_at => @time)
75
+
76
+ @index.save(@user)
77
+
78
+ Dynamoid::Adapter.read("dynamoid_tests_index_user_last_logged_in_ats_and_names", 'Josh', :range_key => @time.to_f)[:ids].should == Set[@user.id]
79
+ end
80
+
81
+ it 'deletes an object from the index it is associated with' do
82
+ @index = Dynamoid::Indexes::Index.new(User, :name)
83
+ @user = User.create(:name => 'Josh', :password => 'test123', :last_logged_in_at => @time, :id => 'test123')
84
+
85
+ @index.save(@user)
86
+ @index.delete(@user)
87
+
88
+ Dynamoid::Adapter.read("dynamoid_tests_index_user_names", 'Josh')[:ids].should be_nil
89
+ end
90
+
91
+ it 'updates an object by removing it from its previous index and adding it to its new one' do
92
+ @index = Dynamoid::Indexes::Index.new(User, :name)
93
+ @user = User.create(:name => 'Josh', :password => 'test123', :last_logged_in_at => @time, :id => 'test123')
94
+
95
+ Dynamoid::Adapter.read("dynamoid_tests_index_user_names", 'Josh')[:ids].should == Set['test123']
96
+ Dynamoid::Adapter.read("dynamoid_tests_index_user_names", 'Justin').should be_nil
97
+
98
+ @user.update_attributes(:name => 'Justin')
99
+
100
+ Dynamoid::Adapter.read("dynamoid_tests_index_user_names", 'Josh')[:ids].should be_nil
101
+ Dynamoid::Adapter.read("dynamoid_tests_index_user_names", 'Justin')[:ids].should == Set['test123']
102
+ end
103
+
104
+ end
@@ -0,0 +1,25 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "Dynamoid::Indexes" do
4
+
5
+ it 'adds indexes to the index array' do
6
+ User.indexes.keys.should =~ [[:created_at], [:created_at, :name], [:email], [:email, :name], [:last_logged_in_at, :name], [:name]]
7
+ end
8
+
9
+ it 'saves indexes to their tables' do
10
+ @user = User.new(:name => 'Josh')
11
+
12
+ User.indexes.each {|k, v| v.expects(:save).with(@user).once.returns(true)}
13
+
14
+ @user.save
15
+ end
16
+
17
+ it 'deletes indexes when the object is destroyed' do
18
+ @user = User.create(:name => 'Josh')
19
+
20
+ User.indexes.each {|k, v| v.expects(:delete).with(@user).once.returns(true)}
21
+
22
+ @user.destroy
23
+ end
24
+
25
+ end
@@ -0,0 +1,301 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "Dynamoid::Persistence" do
4
+
5
+ before do
6
+ Random.stubs(:rand).with(Dynamoid::Config.partition_size).returns(0)
7
+ @address = Address.new
8
+ end
9
+
10
+ context 'without AWS keys' do
11
+ unless ENV['ACCESS_KEY'] && ENV['SECRET_KEY']
12
+ before do
13
+ Dynamoid::Adapter.delete_table(Address.table_name) if Dynamoid::Adapter.list_tables.include?(Address.table_name)
14
+ end
15
+
16
+ it 'creates a table' do
17
+ Address.create_table(:table_name => Address.table_name)
18
+
19
+ Dynamoid::Adapter.list_tables.should include 'dynamoid_tests_addresses'
20
+ end
21
+
22
+ it 'checks if a table already exists' do
23
+ Address.create_table(:table_name => Address.table_name)
24
+
25
+ Address.table_exists?(Address.table_name).should be_true
26
+ Address.table_exists?('crazytable').should be_false
27
+ end
28
+ end
29
+ end
30
+
31
+ it 'assigns itself an id on save' do
32
+ @address.save
33
+
34
+ Dynamoid::Adapter.read("dynamoid_tests_addresses", @address.id)[:id].should == @address.id
35
+ end
36
+
37
+ it 'prevents concurrent writes to tables with a lock_version' do
38
+ @address.save!
39
+ a1 = @address
40
+ a2 = Address.find(@address.id)
41
+
42
+ a1.city = 'Seattle'
43
+ a2.city = 'San Francisco'
44
+
45
+ a1.save!
46
+ expect { a2.save! }.to raise_exception(Dynamoid::Errors::ConditionalCheckFailedException)
47
+ end
48
+
49
+ configured_with 'partitioning' do
50
+ it 'raises an error when attempting to use optimistic locking' do
51
+ expect { address.save! }.to raise_exception
52
+ end
53
+ end
54
+
55
+ it 'assigns itself an id on save only if it does not have one' do
56
+ @address.id = 'test123'
57
+ @address.save
58
+
59
+ Dynamoid::Adapter.read("dynamoid_tests_addresses", 'test123').should_not be_empty
60
+ end
61
+
62
+ it 'has a table name' do
63
+ Address.table_name.should == 'dynamoid_tests_addresses'
64
+ end
65
+
66
+ it 'saves indexes along with itself' do
67
+ @user = User.new(:name => 'Josh')
68
+
69
+ @user.expects(:save_indexes).once.returns(true)
70
+ @user.save
71
+ end
72
+
73
+ it 'deletes an item completely' do
74
+ @user = User.create(:name => 'Josh')
75
+ @user.destroy
76
+
77
+ Dynamoid::Adapter.read("dynamoid_tests_users", @user.id).should be_nil
78
+ end
79
+
80
+ it 'keeps string attributes as strings' do
81
+ @user = User.new(:name => 'Josh')
82
+ @user.send(:dump)[:name].should == 'Josh'
83
+ end
84
+
85
+ it 'dumps datetime attributes' do
86
+ @user = User.create(:name => 'Josh')
87
+ @user.send(:dump)[:name].should == 'Josh'
88
+ end
89
+
90
+ it 'dumps integer attributes' do
91
+ @subscription = Subscription.create(:length => 10)
92
+ @subscription.send(:dump)[:length].should == 10
93
+ end
94
+
95
+ it 'dumps set attributes' do
96
+ @subscription = Subscription.create(:length => 10)
97
+ @magazine = @subscription.magazine.create
98
+
99
+ @subscription.send(:dump)[:magazine_ids].should == Set[@magazine.id]
100
+ end
101
+
102
+ it 'handles nil attributes properly' do
103
+ Address.undump(nil).should be_a(Hash)
104
+ end
105
+
106
+ it 'dumps and undump a serialized field' do
107
+ @address.options = (hash = {:x => [1, 2], "foobar" => 3.14})
108
+ Address.undump(@address.send(:dump))[:options].should == hash
109
+ end
110
+
111
+ [true, false].each do |bool|
112
+ it "dumps a #{bool} boolean field" do
113
+ @address.deliverable = bool
114
+ Address.undump(@address.send(:dump))[:deliverable].should == bool
115
+ end
116
+ end
117
+
118
+ it 'raises on an invalid boolean value' do
119
+ expect do
120
+ @address.deliverable = true
121
+ data = @address.send(:dump)
122
+ data[:deliverable] = 'foo'
123
+ Address.undump(data)
124
+ end.to raise_error(ArgumentError)
125
+ end
126
+
127
+ it 'loads a hash into a serialized field' do
128
+ hash = {foo: :bar}
129
+ Address.new(options: hash).options.should == hash
130
+ end
131
+
132
+ it 'loads attributes from a hash' do
133
+ @time = DateTime.now
134
+ @hash = {:name => 'Josh', :created_at => @time.to_f}
135
+
136
+ User.undump(@hash)[:name].should == 'Josh'
137
+ User.undump(@hash)[:created_at].to_f == @time.to_f
138
+ end
139
+
140
+ it 'runs the before_create callback only once' do
141
+ CamelCase.any_instance.expects(:doing_before_create).once.returns(true)
142
+
143
+ CamelCase.create
144
+ end
145
+
146
+ it 'runs after save callbacks when doing #create' do
147
+ CamelCase.any_instance.expects(:doing_after_create).once.returns(true)
148
+
149
+ CamelCase.create
150
+ end
151
+
152
+ it 'runs after save callbacks when doing #save' do
153
+ CamelCase.any_instance.expects(:doing_after_create).once.returns(true)
154
+
155
+ CamelCase.new.save
156
+ end
157
+
158
+ it 'works with a HashWithIndifferentAccess' do
159
+ hash = ActiveSupport::HashWithIndifferentAccess.new("city" => "Atlanta")
160
+
161
+ lambda {Address.create(hash)}.should_not raise_error
162
+ end
163
+
164
+ context 'create' do
165
+ {
166
+ Tweet => ['with range', { :tweet_id => 1, :group => 'abc' }],
167
+ Message => ['without range', { :message_id => 1, :text => 'foo', :time => DateTime.now }]
168
+ }.each_pair do |clazz, fields|
169
+ it "checks for existence of an existing object #{fields[0]}" do
170
+ t1 = clazz.new(fields[1])
171
+ t2 = clazz.new(fields[1])
172
+
173
+ t1.save
174
+ expect do
175
+ t2.save!
176
+ end.to raise_exception Dynamoid::Errors::ConditionalCheckFailedException
177
+ end
178
+ end
179
+ end
180
+
181
+ context 'unknown fields' do
182
+ let(:clazz) do
183
+ Class.new do
184
+ include Dynamoid::Document
185
+ table :name => :addresses
186
+
187
+ field :city
188
+ field :options, :serialized
189
+ field :deliverable, :bad_type_specifier
190
+ end
191
+ end
192
+
193
+ it 'raises when undumping a column with an unknown field type' do
194
+ expect do
195
+ clazz.new(:deliverable => true) #undump is called here
196
+ end.to raise_error(ArgumentError)
197
+ end
198
+
199
+ it 'raises when dumping a column with an unknown field type' do
200
+ doc = clazz.new
201
+ doc.deliverable = true
202
+ expect do
203
+ doc.dump
204
+ end.to raise_error(ArgumentError)
205
+ end
206
+ end
207
+
208
+ context 'update' do
209
+
210
+ before :each do
211
+ @tweet = Tweet.create(:tweet_id => 1, :group => 'abc', :count => 5, :tags => ['db', 'sql'], :user_name => 'john')
212
+ end
213
+
214
+ it 'runs before_update callbacks when doing #update' do
215
+ CamelCase.any_instance.expects(:doing_before_update).once.returns(true)
216
+
217
+ CamelCase.create(:color => 'blue').update do |t|
218
+ t.set(:color => 'red')
219
+ end
220
+ end
221
+
222
+ it 'runs after_update callbacks when doing #update' do
223
+ CamelCase.any_instance.expects(:doing_after_update).once.returns(true)
224
+
225
+ CamelCase.create(:color => 'blue').update do |t|
226
+ t.set(:color => 'red')
227
+ end
228
+ end
229
+
230
+ it 'support add/delete operation on a field' do
231
+ @tweet.update do |t|
232
+ t.add(:count => 3)
233
+ t.delete(:tags => ['db'])
234
+ end
235
+
236
+ @tweet.count.should eq(8)
237
+ @tweet.tags.to_a.should eq(['sql'])
238
+ end
239
+
240
+ it 'checks the conditions on update' do
241
+ @tweet.update(:if => { :count => 5 }) do |t|
242
+ t.add(:count => 3)
243
+ end.should be_true
244
+
245
+ @tweet.count.should eq(8)
246
+
247
+ @tweet.update(:if => { :count => 5 }) do |t|
248
+ t.add(:count => 3)
249
+ end.should be_false
250
+
251
+ @tweet.count.should eq(8)
252
+
253
+ expect {
254
+ @tweet.update!(:if => { :count => 5 }) do |t|
255
+ t.add(:count => 3)
256
+ end
257
+ }.to raise_error(Dynamoid::Errors::ConditionalCheckFailedException)
258
+ end
259
+
260
+ it 'prevents concurrent saves to tables with a lock_version' do
261
+ @address.save!
262
+ a2 = Address.find(@address.id)
263
+ a2.update! { |a| a.set(:city => "Chicago") }
264
+
265
+ expect do
266
+ @address.city = "Seattle"
267
+ @address.save!
268
+ end.to raise_error(Dynamoid::Errors::ConditionalCheckFailedException)
269
+ end
270
+
271
+ end
272
+
273
+ context 'delete' do
274
+ it 'deletes model with datetime range key' do
275
+ lambda {
276
+ msg = Message.create!(:message_id => 1, :time => DateTime.now, :text => "Hell yeah")
277
+ msg.destroy
278
+ }.should_not raise_error
279
+ end
280
+ end
281
+
282
+ context 'single table inheritance' do
283
+ let(:car) { Car.create(power_locks: false) }
284
+ let(:sub) { NuclearSubmarine.create(torpedoes: 5) }
285
+
286
+ it 'saves subclass objects in the parent table' do
287
+ c = car
288
+ Vehicle.find(c.id).should == c
289
+ end
290
+
291
+ it 'loads subclass item when querying the parent table' do
292
+ c = car
293
+ s = sub
294
+
295
+ Vehicle.all.to_a.tap { |v|
296
+ v.should include(c)
297
+ v.should include(s)
298
+ }
299
+ end
300
+ end
301
+ end