cassandra_mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+