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.
- data/README.rdoc +98 -0
- data/Rakefile.rb +11 -0
- data/lib/cassandra_mapper.rb +5 -0
- data/lib/cassandra_mapper/base.rb +19 -0
- data/lib/cassandra_mapper/connection.rb +9 -0
- data/lib/cassandra_mapper/core_ext/array/extract_options.rb +29 -0
- data/lib/cassandra_mapper/core_ext/array/wrap.rb +22 -0
- data/lib/cassandra_mapper/core_ext/class/inheritable_attributes.rb +232 -0
- data/lib/cassandra_mapper/core_ext/kernel/reporting.rb +62 -0
- data/lib/cassandra_mapper/core_ext/kernel/singleton_class.rb +13 -0
- data/lib/cassandra_mapper/core_ext/module/aliasing.rb +70 -0
- data/lib/cassandra_mapper/core_ext/module/attribute_accessors.rb +66 -0
- data/lib/cassandra_mapper/core_ext/object/duplicable.rb +65 -0
- data/lib/cassandra_mapper/core_ext/string/inflections.rb +160 -0
- data/lib/cassandra_mapper/core_ext/string/multibyte.rb +72 -0
- data/lib/cassandra_mapper/exceptions.rb +10 -0
- data/lib/cassandra_mapper/identity.rb +29 -0
- data/lib/cassandra_mapper/indexing.rb +465 -0
- data/lib/cassandra_mapper/observable.rb +36 -0
- data/lib/cassandra_mapper/persistence.rb +309 -0
- data/lib/cassandra_mapper/support/callbacks.rb +136 -0
- data/lib/cassandra_mapper/support/concern.rb +31 -0
- data/lib/cassandra_mapper/support/dependencies.rb +60 -0
- data/lib/cassandra_mapper/support/descendants_tracker.rb +41 -0
- data/lib/cassandra_mapper/support/inflections.rb +58 -0
- data/lib/cassandra_mapper/support/inflector.rb +7 -0
- data/lib/cassandra_mapper/support/inflector/inflections.rb +213 -0
- data/lib/cassandra_mapper/support/inflector/methods.rb +143 -0
- data/lib/cassandra_mapper/support/inflector/transliterate.rb +99 -0
- data/lib/cassandra_mapper/support/multibyte.rb +46 -0
- data/lib/cassandra_mapper/support/multibyte/utils.rb +62 -0
- data/lib/cassandra_mapper/support/observing.rb +218 -0
- data/lib/cassandra_mapper/support/support_callbacks.rb +593 -0
- data/test/test_helper.rb +11 -0
- data/test/unit/callbacks_test.rb +100 -0
- data/test/unit/identity_test.rb +51 -0
- data/test/unit/indexing_test.rb +406 -0
- data/test/unit/observer_test.rb +56 -0
- data/test/unit/persistence_test.rb +561 -0
- 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
|
+
|