lotus-dynamodb 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.travis.yml +9 -0
- data/.yardopts +5 -0
- data/Gemfile +16 -0
- data/LICENSE.md +22 -0
- data/Procfile +1 -0
- data/README.md +112 -0
- data/Rakefile +17 -0
- data/benchmarks/coercer.rb +76 -0
- data/examples/Gemfile +2 -0
- data/examples/purchase.rb +164 -0
- data/lib/lotus/dynamodb/config.rb +14 -0
- data/lib/lotus/dynamodb/version.rb +8 -0
- data/lib/lotus/model/adapters/dynamodb/coercer.rb +211 -0
- data/lib/lotus/model/adapters/dynamodb/collection.rb +321 -0
- data/lib/lotus/model/adapters/dynamodb/command.rb +117 -0
- data/lib/lotus/model/adapters/dynamodb/query.rb +559 -0
- data/lib/lotus/model/adapters/dynamodb_adapter.rb +190 -0
- data/lib/lotus-dynamodb.rb +3 -0
- data/lotus-dynamodb.gemspec +30 -0
- data/test/fixtures.rb +75 -0
- data/test/model/adapters/dynamodb/coercer_test.rb +269 -0
- data/test/model/adapters/dynamodb/query_test.rb +259 -0
- data/test/model/adapters/dynamodb_adapter_test.rb +940 -0
- data/test/test_helper.rb +46 -0
- data/test/version_test.rb +7 -0
- metadata +203 -0
@@ -0,0 +1,940 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Lotus::Model::Adapters::DynamodbAdapter do
|
4
|
+
before do
|
5
|
+
TestUser = Struct.new(:id, :name, :age) do
|
6
|
+
include Lotus::Entity
|
7
|
+
end
|
8
|
+
|
9
|
+
TestDevice = Struct.new(:id, :created_at) do
|
10
|
+
include Lotus::Entity
|
11
|
+
end
|
12
|
+
|
13
|
+
TestPurchase = Struct.new(:id, :region, :subtotal, :item_ids, :content, :created_at) do
|
14
|
+
include Lotus::Entity
|
15
|
+
end
|
16
|
+
|
17
|
+
coercer = Lotus::Model::Adapters::Dynamodb::Coercer
|
18
|
+
@mapper = Lotus::Model::Mapper.new(coercer) do
|
19
|
+
collection :test_users do
|
20
|
+
entity TestUser
|
21
|
+
|
22
|
+
attribute :id, String
|
23
|
+
attribute :name, String
|
24
|
+
attribute :age, Integer
|
25
|
+
end
|
26
|
+
|
27
|
+
collection :test_devices do
|
28
|
+
entity TestDevice
|
29
|
+
|
30
|
+
attribute :id, String, as: :uuid
|
31
|
+
attribute :created_at, Time
|
32
|
+
|
33
|
+
identity :uuid
|
34
|
+
end
|
35
|
+
|
36
|
+
collection :test_purchases do
|
37
|
+
entity TestPurchase
|
38
|
+
|
39
|
+
attribute :id, String, as: :uuid
|
40
|
+
attribute :region, String
|
41
|
+
attribute :subtotal, Float
|
42
|
+
attribute :item_ids, Set
|
43
|
+
attribute :content, AWS::DynamoDB::Binary
|
44
|
+
attribute :created_at, Time
|
45
|
+
|
46
|
+
identity :uuid
|
47
|
+
end
|
48
|
+
end.load!
|
49
|
+
|
50
|
+
@adapter = Lotus::Model::Adapters::DynamodbAdapter.new(@mapper)
|
51
|
+
@adapter.clear(collection)
|
52
|
+
end
|
53
|
+
|
54
|
+
after do
|
55
|
+
Object.send(:remove_const, :TestUser)
|
56
|
+
Object.send(:remove_const, :TestDevice)
|
57
|
+
Object.send(:remove_const, :TestPurchase)
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:collection) { :test_users }
|
61
|
+
|
62
|
+
describe '#first' do
|
63
|
+
it 'raises an error' do
|
64
|
+
-> { @adapter.first(collection) }.must_raise NotImplementedError
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#last' do
|
69
|
+
it 'raises an error' do
|
70
|
+
-> { @adapter.last(collection) }.must_raise NotImplementedError
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'multiple collections' do
|
75
|
+
it 'create records' do
|
76
|
+
user = TestUser.new
|
77
|
+
device = TestDevice.new(created_at: Time.new)
|
78
|
+
|
79
|
+
@adapter.clear(:test_users)
|
80
|
+
@adapter.clear(:test_devices)
|
81
|
+
|
82
|
+
@adapter.create(:test_users, user)
|
83
|
+
@adapter.create(:test_devices, device)
|
84
|
+
|
85
|
+
@adapter.all(:test_users).must_equal [user]
|
86
|
+
@adapter.all(:test_devices).must_equal [device]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#persist' do
|
91
|
+
describe 'when the given entity is not persisted' do
|
92
|
+
let(:entity) { TestUser.new }
|
93
|
+
|
94
|
+
it 'stores the record and assigns an id' do
|
95
|
+
@adapter.persist(collection, entity)
|
96
|
+
|
97
|
+
entity.id.wont_be_nil
|
98
|
+
@adapter.find(collection, entity.id).must_equal entity
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe 'when the given entity is persisted' do
|
103
|
+
before do
|
104
|
+
@adapter.create(collection, entity)
|
105
|
+
end
|
106
|
+
|
107
|
+
let(:entity) { TestUser.new }
|
108
|
+
|
109
|
+
it 'updates the record and leaves untouched the id' do
|
110
|
+
id = entity.id
|
111
|
+
id.wont_be_nil
|
112
|
+
|
113
|
+
entity.name = 'L'
|
114
|
+
@adapter.persist(collection, entity)
|
115
|
+
|
116
|
+
entity.id.must_equal(id)
|
117
|
+
@adapter.find(collection, entity.id).must_equal entity
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe '#create' do
|
123
|
+
let(:entity) { TestUser.new }
|
124
|
+
|
125
|
+
it 'stores the record and assigns an id' do
|
126
|
+
@adapter.create(collection, entity)
|
127
|
+
|
128
|
+
entity.id.wont_be_nil
|
129
|
+
@adapter.find(collection, entity.id).must_equal entity
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe '#update' do
|
134
|
+
before do
|
135
|
+
@adapter.create(collection, entity)
|
136
|
+
end
|
137
|
+
|
138
|
+
let(:entity) { TestUser.new(id: nil, name: 'L') }
|
139
|
+
|
140
|
+
it 'stores the changes and leave the id untouched' do
|
141
|
+
id = entity.id
|
142
|
+
|
143
|
+
entity.name = 'MG'
|
144
|
+
@adapter.update(collection, entity)
|
145
|
+
|
146
|
+
entity.id.must_equal id
|
147
|
+
@adapter.find(collection, entity.id).must_equal entity
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe '#delete' do
|
152
|
+
before do
|
153
|
+
@adapter.create(collection, entity)
|
154
|
+
end
|
155
|
+
|
156
|
+
let(:entity) { TestUser.new }
|
157
|
+
|
158
|
+
it 'removes the given identity' do
|
159
|
+
@adapter.delete(collection, entity)
|
160
|
+
@adapter.find(collection, entity.id).must_be_nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe '#all' do
|
165
|
+
describe 'when no records are persisted' do
|
166
|
+
it 'returns an empty collection' do
|
167
|
+
@adapter.all(collection).must_be_empty
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe 'when some records are persisted' do
|
172
|
+
before do
|
173
|
+
@adapter.create(collection, entity)
|
174
|
+
end
|
175
|
+
|
176
|
+
let(:entity) { TestUser.new }
|
177
|
+
|
178
|
+
it 'returns all of them' do
|
179
|
+
@adapter.all(collection).must_equal [entity]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe '#find' do
|
185
|
+
before do
|
186
|
+
@adapter.create(collection, entity)
|
187
|
+
end
|
188
|
+
|
189
|
+
describe 'simple key' do
|
190
|
+
let(:entity) { TestUser.new }
|
191
|
+
|
192
|
+
it 'returns the record by id' do
|
193
|
+
@adapter.find(collection, entity.id).must_equal entity
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'returns nil when the record cannot be found' do
|
197
|
+
@adapter.find(collection, 1_000_000.to_s).must_be_nil
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'returns nil when the given id is nil' do
|
201
|
+
@adapter.find(collection, nil).must_be_nil
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'returns nil when the given id is empty string' do
|
205
|
+
@adapter.find(collection, nil).must_be_nil
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe 'complex key' do
|
210
|
+
let(:entity) { TestDevice.new(created_at: Time.new) }
|
211
|
+
let(:collection) { :test_devices }
|
212
|
+
|
213
|
+
it 'returns the record by id' do
|
214
|
+
@adapter.find(collection, entity.id, entity.created_at).must_equal entity
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'returns nil when not enough keys' do
|
218
|
+
@adapter.find(collection, entity.id).must_be_nil
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe '#clear' do
|
224
|
+
before do
|
225
|
+
@adapter.create(collection, entity)
|
226
|
+
end
|
227
|
+
|
228
|
+
let(:entity) { TestUser.new }
|
229
|
+
|
230
|
+
it 'removes all the records' do
|
231
|
+
@adapter.clear(collection)
|
232
|
+
@adapter.all(collection).must_be_empty
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe '#query' do
|
237
|
+
let(:collection) { :test_purchases }
|
238
|
+
let(:purchase1) do
|
239
|
+
TestPurchase.new(
|
240
|
+
region: 'europe',
|
241
|
+
subtotal: 15.0,
|
242
|
+
item_ids: [1, 2, 3],
|
243
|
+
content: "OMG",
|
244
|
+
created_at: Time.new,
|
245
|
+
)
|
246
|
+
end
|
247
|
+
let(:purchase2) do
|
248
|
+
TestPurchase.new(
|
249
|
+
region: 'europe',
|
250
|
+
subtotal: 10.0,
|
251
|
+
item_ids: ["2", "3", "4"],
|
252
|
+
content: AWS::DynamoDB::Binary.new("SO"),
|
253
|
+
created_at: Time.new,
|
254
|
+
)
|
255
|
+
end
|
256
|
+
let(:purchase3) do
|
257
|
+
TestPurchase.new(
|
258
|
+
region: 'usa',
|
259
|
+
subtotal: 5.0,
|
260
|
+
item_ids: [AWS::DynamoDB::Binary.new("WOW")],
|
261
|
+
content: AWS::DynamoDB::Binary.new("MUCH"),
|
262
|
+
created_at: Time.new,
|
263
|
+
)
|
264
|
+
end
|
265
|
+
let(:purchase4) do
|
266
|
+
TestPurchase.new(
|
267
|
+
region: 'asia',
|
268
|
+
subtotal: 100.0,
|
269
|
+
item_ids: [4, 5, 6],
|
270
|
+
content: AWS::DynamoDB::Binary.new("CONTENT"),
|
271
|
+
created_at: Time.new,
|
272
|
+
)
|
273
|
+
end
|
274
|
+
let(:purchase5) do
|
275
|
+
TestPurchase.new(
|
276
|
+
region: 'europe',
|
277
|
+
subtotal: 1.0,
|
278
|
+
created_at: Time.new,
|
279
|
+
)
|
280
|
+
end
|
281
|
+
let(:purchases) { [purchase1, purchase2, purchase3, purchase4] }
|
282
|
+
|
283
|
+
describe 'types' do
|
284
|
+
before do
|
285
|
+
purchases.each do |purchase|
|
286
|
+
@adapter.create(collection, purchase)
|
287
|
+
end
|
288
|
+
|
289
|
+
@purchases = @adapter.query(collection).all.sort_by(&:created_at)
|
290
|
+
end
|
291
|
+
|
292
|
+
it 'has string type' do
|
293
|
+
@purchases.first.region.class.must_equal String
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'has number type' do
|
297
|
+
@purchases.first.subtotal.class.must_equal Float
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'has set type' do
|
301
|
+
@purchases.first.item_ids.class.must_equal Set
|
302
|
+
@purchases.at(0).item_ids.map(&:class).must_equal [BigDecimal, BigDecimal, BigDecimal]
|
303
|
+
@purchases.at(1).item_ids.map(&:class).must_equal [String, String, String]
|
304
|
+
@purchases.at(2).item_ids.map(&:class).must_equal [AWS::DynamoDB::Binary]
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'has binary type' do
|
308
|
+
@purchases.each do |purchase|
|
309
|
+
purchase.content.class.must_equal AWS::DynamoDB::Binary
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe 'where' do
|
315
|
+
describe 'with an empty collection' do
|
316
|
+
it 'returns an empty result set' do
|
317
|
+
result = @adapter.query(collection) do
|
318
|
+
where(region: 'europe')
|
319
|
+
end.all
|
320
|
+
|
321
|
+
result.must_be_empty
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
describe 'with a filled collection' do
|
326
|
+
before do
|
327
|
+
purchases.each do |purchase|
|
328
|
+
@adapter.create(collection, purchase)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
describe 'without key schema' do
|
333
|
+
it 'returns selected records' do
|
334
|
+
query = Proc.new {
|
335
|
+
where(subtotal: 100.0)
|
336
|
+
}
|
337
|
+
|
338
|
+
result = @adapter.query(collection, &query).all
|
339
|
+
result.must_equal [purchase4]
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
describe 'with key schema' do
|
344
|
+
it 'returns selected records' do
|
345
|
+
query = Proc.new {
|
346
|
+
where(region: 'europe')
|
347
|
+
}
|
348
|
+
|
349
|
+
result = @adapter.query(collection, &query).all
|
350
|
+
result.must_equal [purchase1, purchase2]
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'can use multiple where conditions' do
|
354
|
+
created_at = purchase1.created_at
|
355
|
+
query = Proc.new {
|
356
|
+
where(region: 'europe').where(created_at: created_at)
|
357
|
+
}
|
358
|
+
|
359
|
+
result = @adapter.query(collection, &query).all
|
360
|
+
result.must_equal [purchase1]
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'can use array as where condition' do
|
364
|
+
query = Proc.new {
|
365
|
+
where(region: 'europe').where(subtotal: [15.0, 10.0])
|
366
|
+
}
|
367
|
+
|
368
|
+
result = @adapter.query(collection, &query).all
|
369
|
+
result.must_equal [purchase1, purchase2]
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'can use range as where condition' do
|
373
|
+
query = Proc.new {
|
374
|
+
where(region: 'europe').where(subtotal: 8..14)
|
375
|
+
}
|
376
|
+
|
377
|
+
result = @adapter.query(collection, &query).all
|
378
|
+
result.must_equal [purchase2]
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'can use "eq" alias' do
|
382
|
+
query = Proc.new {
|
383
|
+
eq(region: 'europe')
|
384
|
+
}
|
385
|
+
|
386
|
+
@adapter.query(collection, &query).count.must_equal 2
|
387
|
+
end
|
388
|
+
|
389
|
+
it 'can use "in" alias' do
|
390
|
+
@adapter.create(collection, purchase5)
|
391
|
+
|
392
|
+
query = Proc.new {
|
393
|
+
where(region: 'europe').in(subtotal: [10.0, 1.0])
|
394
|
+
}
|
395
|
+
|
396
|
+
@adapter.query(collection, &query).count.must_equal 2
|
397
|
+
end
|
398
|
+
|
399
|
+
it 'can use "between" alias' do
|
400
|
+
@adapter.create(collection, purchase5)
|
401
|
+
|
402
|
+
query = Proc.new {
|
403
|
+
where(region: 'europe').between(subtotal: 0.0..11.0)
|
404
|
+
}
|
405
|
+
|
406
|
+
@adapter.query(collection, &query).count.must_equal 2
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
describe 'exclude' do
|
413
|
+
describe 'with an empty collection' do
|
414
|
+
it 'returns an empty result set' do
|
415
|
+
result = @adapter.query(collection) do
|
416
|
+
exclude(subtotal: 10.0)
|
417
|
+
end.all
|
418
|
+
|
419
|
+
result.must_be_empty
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
describe 'with a filled collection' do
|
424
|
+
before do
|
425
|
+
purchases.each do |purchase|
|
426
|
+
@adapter.create(collection, purchase)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
describe 'without key schema' do
|
431
|
+
it 'returns selected records' do
|
432
|
+
query = Proc.new {
|
433
|
+
exclude(subtotal: 100.0)
|
434
|
+
}
|
435
|
+
|
436
|
+
result = @adapter.query(collection, &query).all
|
437
|
+
result.must_include purchase1
|
438
|
+
result.must_include purchase2
|
439
|
+
result.must_include purchase3
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
describe 'with key schema' do
|
444
|
+
it 'returns selected records' do
|
445
|
+
query = Proc.new {
|
446
|
+
where(region: 'europe').exclude(subtotal: 15.0)
|
447
|
+
}
|
448
|
+
|
449
|
+
result = @adapter.query(collection, &query).all
|
450
|
+
result.must_equal [purchase2]
|
451
|
+
end
|
452
|
+
|
453
|
+
it 'can use multiple exclude conditions' do
|
454
|
+
id = purchase2.id
|
455
|
+
|
456
|
+
query = Proc.new {
|
457
|
+
where(region: 'europe').exclude(subtotal: 15.0).exclude(uuid: id)
|
458
|
+
}
|
459
|
+
|
460
|
+
result = @adapter.query(collection, &query).all
|
461
|
+
result.must_equal []
|
462
|
+
end
|
463
|
+
|
464
|
+
it 'can use multiple exclude conditions with "not" alias' do
|
465
|
+
id = purchase2.id
|
466
|
+
|
467
|
+
query = Proc.new {
|
468
|
+
where(region: 'europe').not(subtotal: 15.0).not(uuid: id)
|
469
|
+
}
|
470
|
+
|
471
|
+
result = @adapter.query(collection, &query).all
|
472
|
+
result.must_equal []
|
473
|
+
end
|
474
|
+
|
475
|
+
it 'can not use array as exclude condition' do
|
476
|
+
query = Proc.new {
|
477
|
+
exclude(subtotal: [15.0, 10.0])
|
478
|
+
}
|
479
|
+
|
480
|
+
->{ @adapter.query(collection, &query).all }.must_raise \
|
481
|
+
NotImplementedError
|
482
|
+
end
|
483
|
+
|
484
|
+
it 'can use "ne" alias' do
|
485
|
+
query = Proc.new {
|
486
|
+
where(region: 'europe').ne(subtotal: 1.0)
|
487
|
+
}
|
488
|
+
|
489
|
+
@adapter.query(collection, &query).count.must_equal 2
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
describe 'comparison' do
|
496
|
+
describe 'with an empty collection' do
|
497
|
+
it 'returns an empty result set' do
|
498
|
+
result = @adapter.query(collection) do
|
499
|
+
le(subtotal: 10.0)
|
500
|
+
end.all
|
501
|
+
|
502
|
+
result.must_be_empty
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
describe 'with a filled collection' do
|
507
|
+
before do
|
508
|
+
purchases.each do |purchase|
|
509
|
+
@adapter.create(collection, purchase)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
it 'can use "le" method' do
|
514
|
+
query = Proc.new {
|
515
|
+
where(region: 'europe').le(subtotal: 15.0)
|
516
|
+
}
|
517
|
+
|
518
|
+
@adapter.query(collection, &query).count.must_equal 2
|
519
|
+
end
|
520
|
+
|
521
|
+
it 'can use "lt" method' do
|
522
|
+
query = Proc.new {
|
523
|
+
where(region: 'europe').lt(subtotal: 15.0)
|
524
|
+
}
|
525
|
+
|
526
|
+
@adapter.query(collection, &query).count.must_equal 1
|
527
|
+
end
|
528
|
+
|
529
|
+
it 'can use "ge" method' do
|
530
|
+
query = Proc.new {
|
531
|
+
where(region: 'europe').ge(subtotal: 10.0)
|
532
|
+
}
|
533
|
+
|
534
|
+
@adapter.query(collection, &query).count.must_equal 2
|
535
|
+
end
|
536
|
+
|
537
|
+
it 'can use "gt" method' do
|
538
|
+
query = Proc.new {
|
539
|
+
where(region: 'europe').gt(subtotal: 10.0)
|
540
|
+
}
|
541
|
+
|
542
|
+
@adapter.query(collection, &query).count.must_equal 1
|
543
|
+
end
|
544
|
+
|
545
|
+
it 'can use "contains" method' do
|
546
|
+
query = Proc.new {
|
547
|
+
where(region: 'europe').contains(item_ids: 2)
|
548
|
+
}
|
549
|
+
|
550
|
+
@adapter.query(collection, &query).count.must_equal 1
|
551
|
+
end
|
552
|
+
|
553
|
+
it 'can use "not_contains" method' do
|
554
|
+
skip_for_fake_dynamo
|
555
|
+
query = Proc.new {
|
556
|
+
where(region: 'europe').not_contains(item_ids: '2')
|
557
|
+
}
|
558
|
+
|
559
|
+
@adapter.query(collection, &query).count.must_equal 1
|
560
|
+
end
|
561
|
+
|
562
|
+
it 'can use "begins_with" method' do
|
563
|
+
query = Proc.new {
|
564
|
+
where(region: 'asia').begins_with(content: "CON")
|
565
|
+
}
|
566
|
+
|
567
|
+
@adapter.query(collection, &query).count.must_equal 1
|
568
|
+
end
|
569
|
+
|
570
|
+
it 'can use "null" method' do
|
571
|
+
skip_for_fake_dynamo
|
572
|
+
@adapter.create(collection, purchase5)
|
573
|
+
|
574
|
+
query = Proc.new {
|
575
|
+
where(region: 'europe').null(:content)
|
576
|
+
}
|
577
|
+
|
578
|
+
@adapter.query(collection, &query).count.must_equal 1
|
579
|
+
end
|
580
|
+
|
581
|
+
it 'can use "not_null" method' do
|
582
|
+
skip_for_fake_dynamo
|
583
|
+
@adapter.create(collection, purchase5)
|
584
|
+
|
585
|
+
query = Proc.new {
|
586
|
+
where(region: 'europe').not_null(:content)
|
587
|
+
}
|
588
|
+
|
589
|
+
@adapter.query(collection, &query).count.must_equal 2
|
590
|
+
end
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
describe 'or' do
|
595
|
+
describe 'with an empty collection' do
|
596
|
+
it 'returns an empty result set' do
|
597
|
+
result = @adapter.query(collection) do
|
598
|
+
where(subtotal: 10.0).or.where(uuid: "omg")
|
599
|
+
end.all
|
600
|
+
|
601
|
+
result.must_be_empty
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
describe 'with a filled collection' do
|
606
|
+
before do
|
607
|
+
purchases.each do |purchase|
|
608
|
+
@adapter.create(collection, purchase)
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
it 'returns selected records' do
|
613
|
+
skip_for_fake_dynamo
|
614
|
+
id = purchase1.id
|
615
|
+
|
616
|
+
query = Proc.new {
|
617
|
+
where(region: 'europe').where(subtotal: 10.0).or.where(uuid: id)
|
618
|
+
}
|
619
|
+
|
620
|
+
result = @adapter.query(collection, &query).all
|
621
|
+
result.must_equal [purchase1, purchase2]
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
describe 'select' do
|
627
|
+
describe 'with an empty collection' do
|
628
|
+
it 'returns an empty result' do
|
629
|
+
result = @adapter.query(collection) do
|
630
|
+
select(:subtotal)
|
631
|
+
end.all
|
632
|
+
|
633
|
+
result.must_be_empty
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
describe 'with a filled collection' do
|
638
|
+
before do
|
639
|
+
purchases.each do |purchase|
|
640
|
+
@adapter.create(collection, purchase)
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
it 'returns the selected columns from all the records' do
|
645
|
+
query = Proc.new {
|
646
|
+
select(:subtotal)
|
647
|
+
}
|
648
|
+
|
649
|
+
result = @adapter.query(collection, &query).all
|
650
|
+
purchases.each do |purchase|
|
651
|
+
record = result.find { |r| r.subtotal == purchase.subtotal }
|
652
|
+
record.wont_be_nil
|
653
|
+
record.region.must_be_nil
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
it 'returns only the select of requested records' do
|
658
|
+
query = Proc.new {
|
659
|
+
where(region: 'europe').select(:subtotal)
|
660
|
+
}
|
661
|
+
|
662
|
+
result = @adapter.query(collection, &query).all
|
663
|
+
|
664
|
+
record = result.first
|
665
|
+
record.subtotal.must_equal(purchase1.subtotal)
|
666
|
+
record.region.must_be_nil
|
667
|
+
end
|
668
|
+
|
669
|
+
it 'returns only the multiple select of requested records' do
|
670
|
+
query = Proc.new {
|
671
|
+
where(region: 'europe').select(:subtotal, :region)
|
672
|
+
}
|
673
|
+
|
674
|
+
result = @adapter.query(collection, &query).all
|
675
|
+
|
676
|
+
record = result.first
|
677
|
+
record.subtotal.must_equal(purchase1.subtotal)
|
678
|
+
record.region.must_equal(purchase1.region)
|
679
|
+
record.id.must_be_nil
|
680
|
+
end
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
describe 'order' do
|
685
|
+
let(:collection) { :test_devices }
|
686
|
+
let(:device1) { TestDevice.new(id: 'device', created_at: Time.new) }
|
687
|
+
let(:device2) { TestDevice.new(id: 'device', created_at: Time.new) }
|
688
|
+
let(:devices) { [device1, device2] }
|
689
|
+
|
690
|
+
describe 'asc' do
|
691
|
+
describe 'with an empty collection' do
|
692
|
+
it 'returns an empty result set' do
|
693
|
+
result = @adapter.query(collection) do
|
694
|
+
where(uuid: 'device').asc
|
695
|
+
end.all
|
696
|
+
|
697
|
+
result.must_be_empty
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
describe 'with a filled collection' do
|
702
|
+
before do
|
703
|
+
devices.each do |device|
|
704
|
+
@adapter.create(collection, device)
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
it 'returns sorted records' do
|
709
|
+
query = Proc.new {
|
710
|
+
where(uuid: 'device').asc
|
711
|
+
}
|
712
|
+
|
713
|
+
result = @adapter.query(collection, &query).all
|
714
|
+
result.must_equal [device1, device2]
|
715
|
+
end
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
describe 'desc' do
|
720
|
+
describe 'with an empty collection' do
|
721
|
+
it 'returns an empty result set' do
|
722
|
+
result = @adapter.query(collection) do
|
723
|
+
where(uuid: 'device').desc
|
724
|
+
end.all
|
725
|
+
|
726
|
+
result.must_be_empty
|
727
|
+
end
|
728
|
+
end
|
729
|
+
|
730
|
+
describe 'with a filled collection' do
|
731
|
+
before do
|
732
|
+
devices.each do |device|
|
733
|
+
@adapter.create(collection, device)
|
734
|
+
end
|
735
|
+
end
|
736
|
+
|
737
|
+
it 'returns reverse sorted records' do
|
738
|
+
query = Proc.new {
|
739
|
+
where(uuid: 'device').desc
|
740
|
+
}
|
741
|
+
|
742
|
+
result = @adapter.query(collection, &query).all
|
743
|
+
result.must_equal [device1, device2]
|
744
|
+
end
|
745
|
+
end
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
describe 'limit' do
|
750
|
+
describe 'with an empty collection' do
|
751
|
+
it 'returns an empty result set' do
|
752
|
+
result = @adapter.query(collection) do
|
753
|
+
limit(1)
|
754
|
+
end.all
|
755
|
+
|
756
|
+
result.must_be_empty
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
describe 'with a filled collection' do
|
761
|
+
before do
|
762
|
+
purchases.each do |purchase|
|
763
|
+
@adapter.create(collection, purchase)
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
it 'returns only the number of requested records' do
|
768
|
+
query = Proc.new {
|
769
|
+
where(region: 'europe').limit(1)
|
770
|
+
}
|
771
|
+
|
772
|
+
result = @adapter.query(collection, &query).all
|
773
|
+
result.must_equal [purchase1]
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
describe 'exists?' do
|
779
|
+
describe 'with an empty collection' do
|
780
|
+
it 'returns false' do
|
781
|
+
result = @adapter.query(collection) do
|
782
|
+
where(region: 'wow')
|
783
|
+
end.exists?
|
784
|
+
|
785
|
+
result.must_equal false
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
describe 'with a filled collection' do
|
790
|
+
before do
|
791
|
+
purchases.each do |purchase|
|
792
|
+
@adapter.create(collection, purchase)
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
it 'returns true when there are matched records' do
|
797
|
+
query = Proc.new {
|
798
|
+
where(region: 'asia')
|
799
|
+
}
|
800
|
+
|
801
|
+
result = @adapter.query(collection, &query).exists?
|
802
|
+
result.must_equal true
|
803
|
+
end
|
804
|
+
|
805
|
+
it 'returns false when there are matched records' do
|
806
|
+
query = Proc.new {
|
807
|
+
where(region: 'wtf')
|
808
|
+
}
|
809
|
+
|
810
|
+
result = @adapter.query(collection, &query).exists?
|
811
|
+
result.must_equal false
|
812
|
+
end
|
813
|
+
|
814
|
+
it 'can use "exist?" alias' do
|
815
|
+
query = Proc.new {
|
816
|
+
where(region: 'usa')
|
817
|
+
}
|
818
|
+
|
819
|
+
result = @adapter.query(collection, &query).exist?
|
820
|
+
result.must_equal true
|
821
|
+
end
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
describe 'count' do
|
826
|
+
describe 'with an empty collection' do
|
827
|
+
it 'returns 0' do
|
828
|
+
result = @adapter.query(collection) do
|
829
|
+
all
|
830
|
+
end.count
|
831
|
+
|
832
|
+
result.must_equal 0
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
describe 'with a filled collection' do
|
837
|
+
before do
|
838
|
+
purchases.each do |purchase|
|
839
|
+
@adapter.create(collection, purchase)
|
840
|
+
end
|
841
|
+
end
|
842
|
+
|
843
|
+
it 'returns the count of all the records' do
|
844
|
+
query = Proc.new {
|
845
|
+
all
|
846
|
+
}
|
847
|
+
|
848
|
+
result = @adapter.query(collection, &query).count
|
849
|
+
result.must_equal 4
|
850
|
+
end
|
851
|
+
|
852
|
+
it 'returns the count from an empty query block' do
|
853
|
+
query = Proc.new {
|
854
|
+
}
|
855
|
+
|
856
|
+
result = @adapter.query(collection, &query).count
|
857
|
+
result.must_equal 4
|
858
|
+
end
|
859
|
+
|
860
|
+
it 'returns only the count of requested records' do
|
861
|
+
query = Proc.new {
|
862
|
+
where(region: 'europe')
|
863
|
+
}
|
864
|
+
|
865
|
+
result = @adapter.query(collection, &query).count
|
866
|
+
result.must_equal 2
|
867
|
+
end
|
868
|
+
end
|
869
|
+
end
|
870
|
+
|
871
|
+
describe 'consistent' do
|
872
|
+
before do
|
873
|
+
purchases.each do |purchase|
|
874
|
+
@adapter.create(collection, purchase)
|
875
|
+
end
|
876
|
+
end
|
877
|
+
|
878
|
+
it 'does not fail' do
|
879
|
+
query = Proc.new {
|
880
|
+
where(region: 'europe').consistent
|
881
|
+
}
|
882
|
+
|
883
|
+
result = @adapter.query(collection, &query).count
|
884
|
+
result.must_equal 2
|
885
|
+
end
|
886
|
+
end
|
887
|
+
|
888
|
+
describe 'index' do
|
889
|
+
describe 'with an empty collection' do
|
890
|
+
it 'returns an empty result for local index' do
|
891
|
+
result = @adapter.query(collection) do
|
892
|
+
index('by_subtotal').where(region: 'europe')
|
893
|
+
end.all
|
894
|
+
|
895
|
+
result.must_be_empty
|
896
|
+
end
|
897
|
+
|
898
|
+
it 'returns an empty result for global index' do
|
899
|
+
result = @adapter.query(collection) do
|
900
|
+
index('by_uuid').where(uuid: 'wow')
|
901
|
+
end.all
|
902
|
+
|
903
|
+
result.must_be_empty
|
904
|
+
end
|
905
|
+
end
|
906
|
+
|
907
|
+
describe 'with a filled collection' do
|
908
|
+
before do
|
909
|
+
purchases.each do |purchase|
|
910
|
+
@adapter.create(collection, purchase)
|
911
|
+
end
|
912
|
+
end
|
913
|
+
|
914
|
+
describe 'local index' do
|
915
|
+
it 'returns selected records' do
|
916
|
+
query = Proc.new {
|
917
|
+
index('by_subtotal').where(region: 'europe')
|
918
|
+
}
|
919
|
+
|
920
|
+
result = @adapter.query(collection, &query).all
|
921
|
+
result.must_equal [purchase2, purchase1]
|
922
|
+
end
|
923
|
+
end
|
924
|
+
|
925
|
+
describe 'global index' do
|
926
|
+
it 'returns selected records' do
|
927
|
+
id = purchase3.id
|
928
|
+
|
929
|
+
query = Proc.new {
|
930
|
+
index('by_uuid').where(uuid: id)
|
931
|
+
}
|
932
|
+
|
933
|
+
result = @adapter.query(collection, &query).all
|
934
|
+
result.must_equal [purchase3]
|
935
|
+
end
|
936
|
+
end
|
937
|
+
end
|
938
|
+
end
|
939
|
+
end
|
940
|
+
end
|