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