cassandra_mapper 0.0.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 (40) hide show
  1. data/README.rdoc +98 -0
  2. data/Rakefile.rb +11 -0
  3. data/lib/cassandra_mapper.rb +5 -0
  4. data/lib/cassandra_mapper/base.rb +19 -0
  5. data/lib/cassandra_mapper/connection.rb +9 -0
  6. data/lib/cassandra_mapper/core_ext/array/extract_options.rb +29 -0
  7. data/lib/cassandra_mapper/core_ext/array/wrap.rb +22 -0
  8. data/lib/cassandra_mapper/core_ext/class/inheritable_attributes.rb +232 -0
  9. data/lib/cassandra_mapper/core_ext/kernel/reporting.rb +62 -0
  10. data/lib/cassandra_mapper/core_ext/kernel/singleton_class.rb +13 -0
  11. data/lib/cassandra_mapper/core_ext/module/aliasing.rb +70 -0
  12. data/lib/cassandra_mapper/core_ext/module/attribute_accessors.rb +66 -0
  13. data/lib/cassandra_mapper/core_ext/object/duplicable.rb +65 -0
  14. data/lib/cassandra_mapper/core_ext/string/inflections.rb +160 -0
  15. data/lib/cassandra_mapper/core_ext/string/multibyte.rb +72 -0
  16. data/lib/cassandra_mapper/exceptions.rb +10 -0
  17. data/lib/cassandra_mapper/identity.rb +29 -0
  18. data/lib/cassandra_mapper/indexing.rb +465 -0
  19. data/lib/cassandra_mapper/observable.rb +36 -0
  20. data/lib/cassandra_mapper/persistence.rb +309 -0
  21. data/lib/cassandra_mapper/support/callbacks.rb +136 -0
  22. data/lib/cassandra_mapper/support/concern.rb +31 -0
  23. data/lib/cassandra_mapper/support/dependencies.rb +60 -0
  24. data/lib/cassandra_mapper/support/descendants_tracker.rb +41 -0
  25. data/lib/cassandra_mapper/support/inflections.rb +58 -0
  26. data/lib/cassandra_mapper/support/inflector.rb +7 -0
  27. data/lib/cassandra_mapper/support/inflector/inflections.rb +213 -0
  28. data/lib/cassandra_mapper/support/inflector/methods.rb +143 -0
  29. data/lib/cassandra_mapper/support/inflector/transliterate.rb +99 -0
  30. data/lib/cassandra_mapper/support/multibyte.rb +46 -0
  31. data/lib/cassandra_mapper/support/multibyte/utils.rb +62 -0
  32. data/lib/cassandra_mapper/support/observing.rb +218 -0
  33. data/lib/cassandra_mapper/support/support_callbacks.rb +593 -0
  34. data/test/test_helper.rb +11 -0
  35. data/test/unit/callbacks_test.rb +100 -0
  36. data/test/unit/identity_test.rb +51 -0
  37. data/test/unit/indexing_test.rb +406 -0
  38. data/test/unit/observer_test.rb +56 -0
  39. data/test/unit/persistence_test.rb +561 -0
  40. metadata +192 -0
@@ -0,0 +1,56 @@
1
+ require 'test_helper'
2
+ class ObserverTest < Test::Unit::TestCase
3
+ context 'A CassandraMapper::Base-derived class' do
4
+ setup do
5
+ @class = Class.new(CassandraMapper::Base)
6
+ @class = CassandraMapper::Base
7
+ @class.maps :key
8
+ end
9
+
10
+ context 'with a registered observer class' do
11
+ setup do
12
+ @observer_class = Class.new(CassandraMapper::Observer)
13
+ @observer_class.observe @class
14
+ @observer = @observer_class.instance
15
+ @instance = @class.new
16
+ @instance.key = 'foo'
17
+ @instance.connection = stub('connection', :insert => @instance)
18
+ end
19
+
20
+ should 'invoke :after_load on observer' do
21
+ @observer.expects(:after_load).with(@instance).returns(true)
22
+ @instance.loaded!
23
+ end
24
+
25
+ should 'invoke :before_save, :before_create, :after_create, :after_save on observer' do
26
+ seq = sequence('callbacks')
27
+ @observer.expects(:before_save).with(@instance).in_sequence(seq).returns(true)
28
+ @observer.expects(:before_create).with(@instance).in_sequence(seq).returns(true)
29
+ @observer.expects(:after_create).with(@instance).in_sequence(seq).returns(true)
30
+ @observer.expects(:after_save).with(@instance).in_sequence(seq).returns(true)
31
+ @instance.stubs(:new_record?).returns(true)
32
+ @instance.save
33
+ end
34
+
35
+ should 'invoke :before_save, :before_update, :after_update, :after_save on observer' do
36
+ seq = sequence('callbacks')
37
+ @observer.expects(:before_save).with(@instance).in_sequence(seq).returns(true)
38
+ @observer.expects(:before_update).with(@instance).in_sequence(seq).returns(true)
39
+ @observer.expects(:after_update).with(@instance).in_sequence(seq).returns(true)
40
+ @observer.expects(:after_save).with(@instance).in_sequence(seq).returns(true)
41
+ @instance.stubs(:new_record?).returns(false)
42
+ @instance.save
43
+ end
44
+
45
+ should 'invoke :before_destroy, :after_destroy on observer for existing record' do
46
+ seq = sequence('callbacks')
47
+ @instance.stubs(:new_record?).returns(false)
48
+ @instance.stubs(:freeze)
49
+ @class.stubs(:delete)
50
+ @observer.expects(:before_destroy).with(@instance).in_sequence(seq).returns(true)
51
+ @observer.expects(:after_destroy).with(@instance).in_sequence(seq).returns(true)
52
+ @instance.destroy
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,561 @@
1
+ require 'test_helper'
2
+
3
+ class PersistenceTest < Test::Unit::TestCase
4
+ context 'CassandraMapper::Base class' do
5
+ setup do
6
+ @class = Class.new(CassandraMapper::Base)
7
+ end
8
+
9
+ should 'provide a column_family class setter' do
10
+ assert_equal nil, @class.column_family
11
+ assert_equal 'SomeColumnFamily', @class.column_family('SomeColumnFamily')
12
+ assert_equal 'SomeColumnFamily', @class.column_family
13
+ end
14
+
15
+ should 'provide a connection class attribute' do
16
+ assert_equal nil, @class.connection
17
+ connection = stub('connection')
18
+ @class.connection = connection
19
+ assert_equal connection, @class.connection
20
+ end
21
+ end
22
+
23
+ context 'CassandraMapper::Base' do
24
+ setup do
25
+ @column_family = 'TestColumnFamily'
26
+ @class = Class.new(CassandraMapper::Base) do
27
+ [:a, :b, :c].each {|name| maps name}
28
+ def key; self.a; end
29
+ end
30
+ @connection = stub('cassandra_client')
31
+ @class.stubs(:connection).returns(@connection)
32
+ @class.stubs(:column_family).returns(@column_family)
33
+ @supercolumn_family = 'TestSuperColumnFamily'
34
+ @supercolumn_class = Class.new(CassandraMapper::Base) do
35
+ [:a, :b, :c].each do |name|
36
+ maps name do
37
+ [:x, :y, :z].each {|inner| maps inner}
38
+ end
39
+ end
40
+
41
+ def key; a.x; end
42
+ end
43
+ @supercolumn_class.stubs(:connection).returns(@connection)
44
+ @supercolumn_class.stubs(:column_family).returns(@supercolumn_family)
45
+ end
46
+
47
+ context 'connection' do
48
+ setup do
49
+ @instance = @class.new
50
+ end
51
+
52
+ should 'use class connection at instance level by default' do
53
+ assert_equal @connection, @instance.connection
54
+ end
55
+
56
+ should 'use instance connection if set' do
57
+ instance_connection = stub('instance_cassandra_client')
58
+ @instance.connection = instance_connection
59
+ assert_equal instance_connection, @instance.connection
60
+ end
61
+ end
62
+
63
+ context 'when saving' do
64
+ setup do
65
+ @values = {'a' => 'Aa', 'b' => 'Bb', 'c' => 'Cc'}
66
+ @instance = @class.new(@values)
67
+ @instance_connection = stub('instance_cassandra_client')
68
+ @instance.stubs(:connection).returns(@instance_connection)
69
+ end
70
+
71
+ context 'to cassandra' do
72
+ context 'a new instance' do
73
+ setup do
74
+ @instance.stubs(:new_record?).returns(true)
75
+ end
76
+
77
+ should 'pass defined attributes to thrift' do
78
+ # The nil result from Cassandra/Thrift is somewhat uninspiring.
79
+ @instance_connection.expects(:insert).with(@column_family, @values['a'], @values).returns(nil)
80
+ assert @instance.save
81
+ end
82
+
83
+ should 'not pass undefined attributes to thrift' do
84
+ @values.delete 'b'
85
+ @instance.b = nil
86
+ @instance_connection.expects(:insert).with(@column_family, @values['a'], @values).returns(nil)
87
+
88
+ assert @instance.save
89
+ end
90
+
91
+ should 'throw an UndefinedKey exception if key attribute is empty' do
92
+ @instance.a = nil
93
+ assert_raise(CassandraMapper::UndefinedKeyException) { @instance.save }
94
+ end
95
+
96
+ should 'set :new_record? to false after the insert' do
97
+ operations = sequence('save operations')
98
+ @instance_connection.expects(:insert).once.in_sequence(operations)
99
+ @instance.expects(:new_record=).with(false).once.in_sequence(operations).returns(false)
100
+ assert @instance.save
101
+ end
102
+ end
103
+
104
+ context 'an existing record instance' do
105
+ setup do
106
+ @instance.stubs(:new_record?).returns(false)
107
+ end
108
+
109
+ should 'be a no-op if no attributes were changed' do
110
+ @instance_connection.expects(:insert).never
111
+ assert_equal false, @instance.save
112
+ end
113
+
114
+ should 'pass only the key/values for attributes that changed to thrift' do
115
+ key = @values['a']
116
+ @instance.b = 'B foo'
117
+ @instance.c = 'C foo'
118
+ expected = {'b' => @instance.b, 'c' => @instance.c}
119
+ @instance_connection.expects(:insert).with(@column_family, key, expected).returns(nil)
120
+ assert_equal @instance, @instance.save
121
+ end
122
+
123
+ should 'prune nil values in the top level to column deletions' do
124
+ key = @values['a']
125
+ @instance.b = nil
126
+ @instance_connection.expects(:remove).with(@column_family, key, 'b').returns(nil)
127
+ @instance.save
128
+ end
129
+
130
+ should 'properly combine insert and remove operations with consistent timestamp' do
131
+ stamp = Time.stamp
132
+ Time.stubs(:stamp).returns(stamp)
133
+ key = @values['a']
134
+ @instance.c = 'C Foo'
135
+ @instance.b = nil
136
+ @instance_connection.expects(:insert).with(@column_family, key, {'c' => 'C Foo'}, :timestamp => stamp).returns(nil)
137
+ @instance_connection.expects(:remove).with(@column_family, key, 'b', :timestamp => stamp).returns(nil)
138
+ @instance.save
139
+ end
140
+
141
+ context 'with supercolumns' do
142
+ setup do
143
+ @supercolumn_values = ['a', 'b', 'c'].inject({}) {|memo, supercol| memo[supercol] = ['x','y','z'].inject({}) {|hash, col| hash[col] = col + supercol; hash}; memo}
144
+ @supercolumn_instance = @supercolumn_class.new(@supercolumn_values)
145
+ @supercolumn_instance.stubs(:connection).returns(@instance_connection)
146
+ @supercolumn_instance.stubs(:new_record?).returns(false)
147
+ end
148
+
149
+ should 'prune nil values in the second level to subcolumn deletions with consistent timestamp' do
150
+ key = @supercolumn_instance.key
151
+ @supercolumn_instance.a.z = nil
152
+ @supercolumn_instance.b.y = nil
153
+ stamp = Time.stamp
154
+ Time.stubs(:stamp).returns(stamp)
155
+ @instance_connection.expects(:remove).with(@supercolumn_family, key, 'a', 'z', :timestamp => stamp)
156
+ @instance_connection.expects(:remove).with(@supercolumn_family, key, 'b', 'y', :timestamp => stamp)
157
+ @instance_connection.expects(:insert).never
158
+ @supercolumn_instance.save
159
+ end
160
+
161
+ should 'prune full supercolumns if top level is nil' do
162
+ key = @supercolumn_values['a']['x']
163
+ @supercolumn_instance.b = nil
164
+ @instance_connection.expects(:remove).with(@supercolumn_family, key, 'b')
165
+ @supercolumn_instance.save
166
+ end
167
+
168
+ should 'combine inserts and deletes properly with consistent timestamp' do
169
+ key = @supercolumn_values['a']['x']
170
+ stamp = Time.stamp
171
+ Time.stubs(:stamp).returns(stamp)
172
+ @supercolumn_instance.a.y = 'ay foo'
173
+ @supercolumn_instance.b.x = 'bx foo'
174
+ @supercolumn_instance.b.y = nil
175
+ @supercolumn_instance.c = nil
176
+ @instance_connection.expects(:insert).with(@supercolumn_family, key, {'a' => {'y' => 'ay foo'}, 'b' => {'x' => 'bx foo'}}, :timestamp => stamp).returns(nil)
177
+ @instance_connection.expects(:remove).with(@supercolumn_family, key, 'b', 'y', :timestamp => stamp)
178
+ @instance_connection.expects(:remove).with(@supercolumn_family, key, 'c', :timestamp => stamp)
179
+ @supercolumn_instance.save
180
+ end
181
+ end
182
+ end
183
+ end
184
+ context 'to a mutation' do
185
+ setup do
186
+ @column_family = :ColumnFamily
187
+ @class.stubs(:column_family).returns(@column_family)
188
+ @supercolumn_family = :SuperColumnFamily
189
+ @supercolumn_class = Class.new(CassandraMapper::Base) do
190
+ [:a, :b, :c].each do |supercol|
191
+ maps supercol do
192
+ [:x, :y, :z].each do |col|
193
+ maps col
194
+ end
195
+ end
196
+ end
197
+ def key; a ? a.x : nil; end
198
+ end
199
+ @supercolumn_class.stubs(:column_family).returns(@supercolumn_family)
200
+ @supercolumn_instance = @supercolumn_class.new(:a => {}, :b => {}, :c => {})
201
+ @supercolumn_instance.stubs(:connection).returns(@instance_connection)
202
+ @timestamp = Time.stamp
203
+ Time.stubs(:stamp).returns(@timestamp)
204
+ end
205
+
206
+ should 'throw an UndefinedKeyException if key is undefined' do
207
+ @instance.stubs(:key).returns(nil)
208
+ assert_raise(CassandraMapper::UndefinedKeyException) { @instance.to_mutation }
209
+ end
210
+
211
+ context 'a new instance' do
212
+ setup do
213
+ @instance.stubs(:new_record?).returns(true)
214
+ @supercolumn_instance.stubs(:new_record?).returns(true)
215
+ end
216
+
217
+ should 'only provide mutations for defined simple columns' do
218
+ @instance.c = nil
219
+ result = @instance.to_mutation
220
+ mutations = result[@values['a']][@column_family.to_s]
221
+ mutations.sort! do |a, b|
222
+ a.column_or_supercolumn.column.name <=> b.column_or_supercolumn.column.name
223
+ end
224
+ assert_equal(
225
+ {
226
+ @values['a'] => {
227
+ @column_family.to_s => ['a', 'b'].collect {|attrib|
228
+ CassandraThrift::Mutation.new(
229
+ :column_or_supercolumn => CassandraThrift::ColumnOrSuperColumn.new(
230
+ :column => CassandraThrift::Column.new(
231
+ :name => attrib,
232
+ :value => @values[attrib],
233
+ :timestamp => @timestamp
234
+ )
235
+ )
236
+ )
237
+ }
238
+ }
239
+ },
240
+ result
241
+ )
242
+ end
243
+
244
+ should 'only provide mutations for defined super/sub columns' do
245
+ @supercolumn_instance.a.x = 'a-x'
246
+ @supercolumn_instance.a.y = 'a-y'
247
+ @supercolumn_instance.b.z = 'b-z'
248
+ result = @supercolumn_instance.to_mutation
249
+ mutations = result['a-x'][@supercolumn_family.to_s]
250
+ mutations.sort! do |a, b|
251
+ a.column_or_supercolumn.super_column.name <=> b.column_or_supercolumn.super_column.name
252
+ end
253
+ mutations.each do |mutation|
254
+ mutation.column_or_supercolumn.super_column.columns.sort! do |a, b|
255
+ a.name <=> b.name
256
+ end
257
+ end
258
+ assert_equal(
259
+ {
260
+ 'a-x' => {@supercolumn_family.to_s => [[:a, :x, :y], [:b, :z]].collect { |args|
261
+ supercol = args.shift.to_s
262
+ CassandraThrift::Mutation.new(
263
+ :column_or_supercolumn => CassandraThrift::ColumnOrSuperColumn.new(
264
+ :super_column => CassandraThrift::SuperColumn.new(
265
+ :name => supercol,
266
+ :columns => args.collect {|col|
267
+ CassandraThrift::Column.new(
268
+ :timestamp => @timestamp,
269
+ :name => col.to_s,
270
+ :value => supercol + '-' + col.to_s
271
+ )
272
+ }
273
+ )
274
+ )
275
+ )
276
+ }
277
+ }},
278
+ result
279
+ )
280
+ end
281
+ end
282
+
283
+ context 'an existing record instance' do
284
+ setup do
285
+ @instance.stubs(:new_record?).returns(false)
286
+ @supercolumn_instance = @supercolumn_class.new(
287
+ @supercolumn_values = {
288
+ 'a' => {'x' => 'a-x', 'y' => 'a-y', 'z' => 'a-z'},
289
+ 'b' => {'x' => 'b-x', 'y' => 'b-y', 'z' => 'b-z'},
290
+ 'c' => {'x' => 'c-x', 'y' => 'c-y', 'z' => 'c-z'}
291
+ }
292
+ )
293
+ @supercolumn_instance.stubs(:new_record?).returns(false)
294
+ end
295
+
296
+ should 'only output mutations for attributes that changed' do
297
+ @instance.b = 'foo'
298
+ assert_equal(
299
+ {
300
+ @values['a'] => {
301
+ @column_family.to_s => [
302
+ CassandraThrift::Mutation.new(
303
+ :column_or_supercolumn => CassandraThrift::ColumnOrSuperColumn.new(
304
+ :column => CassandraThrift::Column.new(
305
+ :name => 'b',
306
+ :value => 'foo',
307
+ :timestamp => @timestamp
308
+ )
309
+ )
310
+ )
311
+ ]
312
+ }
313
+ },
314
+ @instance.to_mutation
315
+ )
316
+ end
317
+
318
+ should 'output deletions for attributes that were set to nil' do
319
+ @instance.b = nil
320
+ @instance.c = nil
321
+ result = @instance.to_mutation
322
+ result[@values['a']][@column_family.to_s].first.deletion.predicate.column_names.sort!
323
+ assert_equal(
324
+ {
325
+ @values['a'] => {
326
+ @column_family.to_s => [
327
+ CassandraThrift::Mutation.new(
328
+ :deletion => CassandraThrift::Deletion.new(
329
+ :super_column => nil,
330
+ :timestamp => @timestamp,
331
+ :predicate => CassandraThrift::SlicePredicate.new(
332
+ :column_names => ['b', 'c']
333
+ )
334
+ )
335
+ )
336
+ ]
337
+ }
338
+ },
339
+ result
340
+ )
341
+ end
342
+
343
+ # to-do: simplemapper doesn't have change tracking quite dialed in for nested
344
+ # structures just yet.
345
+ # should 'only provide mutations for supercolumn/subcol attributes that changed' do
346
+ # @supercolumn_instance.b.y = 'foo'
347
+ # assert_equal(
348
+ # {
349
+ # @supercolumn_values['a']['x'] => {
350
+ # @supercolumn_family.to_s => [
351
+ # CassandraThrift::Mutation.new(
352
+ # :column_or_supercolumn => CassandraThrift::ColumnOrSuperColumn.new(
353
+ # :super_column => CassandraThrift::SuperColumn.new(
354
+ # :name => 'b',
355
+ # :columns => [
356
+ # CassandraThrift::Column.new(
357
+ # :name => 'x',
358
+ # :value => 'foo',
359
+ # :timestamp => @timestamp
360
+ # )
361
+ # ]
362
+ # )
363
+ # )
364
+ # )
365
+ # ]
366
+ # }
367
+ # },
368
+ # @supercolumn_instance.to_mutation
369
+ # )
370
+ # end
371
+ #
372
+ # should 'provide deletions for supercolumn/column attributes that were set to nil' do
373
+ # end
374
+ end
375
+ end
376
+ end
377
+
378
+ context 'retrieving via :find' do
379
+ setup do
380
+ @instances = [:A, :B, :C].inject({}) do |hash, name|
381
+ val = name.to_s
382
+ hash[name.to_s] = @class.new('a' => val, 'b' => 'b' + val, 'c' => 'c' + val)
383
+ hash
384
+ end
385
+ end
386
+
387
+ should 'use multi_get to return a single item for a single found key' do
388
+ key = @instances.keys.first.to_s
389
+ @connection.expects(:multi_get).with(@column_family, [key]).returns({key => @instances[key].to_simple})
390
+ assert_equal @instances[key].to_simple, @class.find(key).to_simple
391
+ end
392
+
393
+ should 'mark :new_record? as false on single item returned' do
394
+ key = @instances.keys.first.to_s
395
+ @connection.stubs(:multi_get).with(@column_family, [key]).returns({key => @instances[key].to_simple})
396
+ assert_equal false, @class.find(key).new_record?
397
+ end
398
+
399
+ should 'use multi_get to return a single item in an array for a single-item key list' do
400
+ key = @instances.keys.first.to_s
401
+ @connection.expects(:multi_get).with(@column_family, [key]).returns({key => @instances[key].to_simple})
402
+ assert_equal [@instances[key].to_simple], @class.find([key]).collect {|x| x.to_simple}
403
+ end
404
+
405
+ should 'mark :new_record? as false on the single item returned' do
406
+ key = @instances.keys.first.to_s
407
+ @connection.stubs(:multi_get).with(@column_family, [key]).returns({key => @instances[key].to_simple})
408
+ assert_equal [false], @class.find([key]).collect {|x| x.new_record?}
409
+ end
410
+
411
+ should 'use multi_get to return an array of items for a multi-key list' do
412
+ keys = @instances.keys.sort
413
+ client_result = @instances.inject({}) {|h, pair| h[pair[0]] = pair[1].to_simple; h}
414
+ @connection.expects(:multi_get).with(@column_family, keys).returns(client_result)
415
+ result = (@class.find(keys) || []).sort_by {|x| x.key}
416
+ assert_equal @instances.values_at(*keys).collect {|x| x.to_simple}, result.collect {|y| y.to_simple}
417
+ end
418
+
419
+ should 'mark :new_record? false on all items returned' do
420
+ keys = @instances.keys
421
+ client_result = @instances.inject({}) {|h, pair| h[pair[0]] = pair[1].to_simple; h}
422
+ @connection.stubs(:multi_get).with(@column_family, keys).returns(client_result)
423
+ assert_equal(keys.collect {|x| false}, @class.find(keys).collect {|y| y.new_record?})
424
+ end
425
+
426
+ context 'with keys that cannot be found' do
427
+ setup do
428
+ @source_keys = @instances.keys
429
+ @keys = @source_keys.clone
430
+ @keys.pop
431
+ @client_result = @instances.values_at(*@keys).inject({}) {|h, i| h[i.key] = i.to_simple; h}
432
+ @connection.expects(:multi_get).with(@column_family, @source_keys).returns(@client_result)
433
+ end
434
+
435
+ should 'throw a RecordNotFound exception' do
436
+ assert_raises(CassandraMapper::RecordNotFoundException) { @class.find(@source_keys) }
437
+ end
438
+
439
+ should 'return subset of objects found if :allow_missing option is true' do
440
+ result = (@class.find(@source_keys, :allow_missing => true) || []).sort_by {|x| x.key}.collect {|o| o.to_simple}
441
+ assert_equal @instances.values_at(*@keys).collect {|y| y.to_simple},
442
+ result
443
+ end
444
+ end
445
+
446
+ should 'throw an InvalidArgument exception if the key list is empty' do
447
+ assert_raises(CassandraMapper::InvalidArgumentException) { @class.find() }
448
+ end
449
+ end
450
+
451
+ context 'removing via class.delete' do
452
+ should 'invoke :remove on underlying client for the given id' do
453
+ @connection.expects(:remove).with(@column_family, key = 'foo')
454
+ # explicity verify: no object is instantiated for this
455
+ @class.expects(:new).never
456
+ @class.delete(key)
457
+ end
458
+
459
+ should 'invoke :remove on underlying client for all ids' do
460
+ # this is suboptimal, but that's what the client offers for now
461
+ keys = ['a', 'b', 'c', 'd']
462
+ keys.each {|key| @connection.expects(:remove).with(@column_family, key)}
463
+ # again, be sure no object is instantiated for this
464
+ @class.expects(:new).never
465
+ @class.delete(keys)
466
+ end
467
+ end
468
+
469
+ context 'removing via instance.delete' do
470
+ setup do
471
+ @object_new = @class.new
472
+ @object_new.stubs(:new_record?).returns(true)
473
+ @key_new = 'new row'
474
+ @object_new.stubs(:key).with.returns(@key_new)
475
+ @object_old = @class.new
476
+ @object_old.stubs(:new_record?).returns(false)
477
+ @key_old = 'old row'
478
+ @object_old.stubs(:key).with.returns(@key_old)
479
+ # intercept freeze calls to prevent mocha issues
480
+ @object_new.stubs(:freeze)
481
+ @object_old.stubs(:freeze)
482
+ end
483
+
484
+ context 'via instance.delete' do
485
+ should 'mark the instance as destroyed' do
486
+ @class.stubs(:delete)
487
+ @object_new.delete
488
+ @object_old.delete
489
+ assert_equal true, @object_new.destroyed?
490
+ assert_equal true, @object_old.destroyed?
491
+ end
492
+
493
+ should 'freeze the instance' do
494
+ @class.stubs(:delete)
495
+ # This would be better with a traditional assertion to check :frozen?,
496
+ # but Mocha has teardown problems that breaks the entire test suite on
497
+ # Ruby 1.8.7 when de-stubbing objects that are now marked as frozen.
498
+ # So we have to use expectations instead.
499
+ @object_new.expects(:freeze).with.once
500
+ @object_old.expects(:freeze).with.once
501
+ @object_new.delete
502
+ @object_old.delete
503
+ end
504
+
505
+ should 'invoke class.delete on existing rows only' do
506
+ seq = sequence('expectations')
507
+ @class.expects(:delete).with(@key_new).never.in_sequence(seq)
508
+ @class.expects(:delete).with(@key_old).once.in_sequence(seq)
509
+ @object_new.delete
510
+ @object_old.delete
511
+ end
512
+ end
513
+
514
+ context 'via instance.destroy' do
515
+ should 'invoke class.delete on existing rows only' do
516
+ seq = sequence('expectations')
517
+ @class.expects(:delete).with(@key_new).never.in_sequence(seq)
518
+ @class.expects(:delete).with(@key_old).once.in_sequence(seq)
519
+ @object_new.destroy
520
+ @object_old.destroy
521
+ end
522
+
523
+ should 'freeze the instance' do
524
+ @class.stubs(:delete)
525
+ # Again, working around Mocha teardown issues, using expectations rather
526
+ # than assertions.
527
+ @object_new.expects(:freeze).with.once
528
+ @object_old.expects(:freeze).with.once
529
+ @object_new.destroy
530
+ @object_old.destroy
531
+ end
532
+
533
+ should 'mark the instance as destroyed' do
534
+ @class.stubs(:delete)
535
+ @object_new.destroy
536
+ @object_old.destroy
537
+ assert_equal true, @object_new.destroyed?
538
+ assert_equal true, @object_old.destroyed?
539
+ end
540
+ end
541
+ end
542
+
543
+ context 'removing via class.destroy' do
544
+ should ':find the object based on given id and :destroy it' do
545
+ @instance = mock('row')
546
+ seq = sequence('order of events')
547
+ @class.expects(:find).with(some_id = 'terrible key').in_sequence(seq).returns(@instance)
548
+ @instance.expects(:destroy).in_sequence(seq)
549
+ @class.destroy(some_id)
550
+ end
551
+
552
+ should ':find all objects from given ids and :destroy them in turn' do
553
+ ids = ['a', 'b', 'c', 'd', 'e']
554
+ instances = ids.collect {|id| x = mock(id); x.expects(:destroy); x}
555
+ @class.expects(:find).with(ids).returns(instances)
556
+ @class.destroy(ids)
557
+ end
558
+ end
559
+ end
560
+ end
561
+