zermelo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +10 -0
  4. data/.travis.yml +27 -0
  5. data/Gemfile +20 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +512 -0
  8. data/Rakefile +1 -0
  9. data/lib/zermelo/associations/association_data.rb +24 -0
  10. data/lib/zermelo/associations/belongs_to.rb +115 -0
  11. data/lib/zermelo/associations/class_methods.rb +244 -0
  12. data/lib/zermelo/associations/has_and_belongs_to_many.rb +128 -0
  13. data/lib/zermelo/associations/has_many.rb +120 -0
  14. data/lib/zermelo/associations/has_one.rb +109 -0
  15. data/lib/zermelo/associations/has_sorted_set.rb +124 -0
  16. data/lib/zermelo/associations/index.rb +50 -0
  17. data/lib/zermelo/associations/index_data.rb +18 -0
  18. data/lib/zermelo/associations/unique_index.rb +44 -0
  19. data/lib/zermelo/backends/base.rb +115 -0
  20. data/lib/zermelo/backends/influxdb_backend.rb +178 -0
  21. data/lib/zermelo/backends/redis_backend.rb +281 -0
  22. data/lib/zermelo/filters/base.rb +235 -0
  23. data/lib/zermelo/filters/influxdb_filter.rb +162 -0
  24. data/lib/zermelo/filters/redis_filter.rb +558 -0
  25. data/lib/zermelo/filters/steps/base_step.rb +22 -0
  26. data/lib/zermelo/filters/steps/diff_range_step.rb +17 -0
  27. data/lib/zermelo/filters/steps/diff_step.rb +17 -0
  28. data/lib/zermelo/filters/steps/intersect_range_step.rb +17 -0
  29. data/lib/zermelo/filters/steps/intersect_step.rb +17 -0
  30. data/lib/zermelo/filters/steps/limit_step.rb +17 -0
  31. data/lib/zermelo/filters/steps/offset_step.rb +17 -0
  32. data/lib/zermelo/filters/steps/sort_step.rb +17 -0
  33. data/lib/zermelo/filters/steps/union_range_step.rb +17 -0
  34. data/lib/zermelo/filters/steps/union_step.rb +17 -0
  35. data/lib/zermelo/locks/no_lock.rb +16 -0
  36. data/lib/zermelo/locks/redis_lock.rb +221 -0
  37. data/lib/zermelo/records/base.rb +62 -0
  38. data/lib/zermelo/records/class_methods.rb +127 -0
  39. data/lib/zermelo/records/collection.rb +14 -0
  40. data/lib/zermelo/records/errors.rb +24 -0
  41. data/lib/zermelo/records/influxdb_record.rb +35 -0
  42. data/lib/zermelo/records/instance_methods.rb +224 -0
  43. data/lib/zermelo/records/key.rb +19 -0
  44. data/lib/zermelo/records/redis_record.rb +27 -0
  45. data/lib/zermelo/records/type_validator.rb +20 -0
  46. data/lib/zermelo/version.rb +3 -0
  47. data/lib/zermelo.rb +102 -0
  48. data/spec/lib/zermelo/associations/belongs_to_spec.rb +6 -0
  49. data/spec/lib/zermelo/associations/has_many_spec.rb +6 -0
  50. data/spec/lib/zermelo/associations/has_one_spec.rb +6 -0
  51. data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +6 -0
  52. data/spec/lib/zermelo/associations/index_spec.rb +6 -0
  53. data/spec/lib/zermelo/associations/unique_index_spec.rb +6 -0
  54. data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
  55. data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
  56. data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
  57. data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
  58. data/spec/lib/zermelo/locks/redis_lock_spec.rb +170 -0
  59. data/spec/lib/zermelo/records/influxdb_record_spec.rb +258 -0
  60. data/spec/lib/zermelo/records/key_spec.rb +6 -0
  61. data/spec/lib/zermelo/records/redis_record_spec.rb +1426 -0
  62. data/spec/lib/zermelo/records/type_validator_spec.rb +6 -0
  63. data/spec/lib/zermelo/version_spec.rb +6 -0
  64. data/spec/lib/zermelo_spec.rb +6 -0
  65. data/spec/spec_helper.rb +67 -0
  66. data/spec/support/profile_all_formatter.rb +44 -0
  67. data/spec/support/uncolored_doc_formatter.rb +74 -0
  68. data/zermelo.gemspec +30 -0
  69. metadata +174 -0
@@ -0,0 +1,1426 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/records/redis_record'
3
+
4
+ # NB: also covers associations.rb, which is mixed in to Zermelo::Record
5
+
6
+ describe Zermelo::Records::RedisRecord, :redis => true do
7
+
8
+ module Zermelo
9
+ class RedisExample
10
+ include Zermelo::Records::RedisRecord
11
+
12
+ define_attributes :name => :string,
13
+ :email => :string,
14
+ :active => :boolean
15
+
16
+ validates :name, :presence => true
17
+
18
+ has_many :children, :class_name => 'Zermelo::RedisExampleChild',
19
+ :inverse_of => :example, :before_add => :fail_if_roger
20
+
21
+ has_sorted_set :data, :class_name => 'Zermelo::RedisExampleDatum',
22
+ :key => :timestamp, :inverse_of => :example
23
+
24
+ has_and_belongs_to_many :templates, :class_name => 'Zermelo::Template',
25
+ :inverse_of => :examples
26
+
27
+ index_by :active
28
+ unique_index_by :name
29
+
30
+ def fail_if_roger(*childs)
31
+ raise "Not adding child" if childs.any? {|c| 'Roger'.eql?(c.name) }
32
+ end
33
+ end
34
+
35
+ class RedisExampleChild
36
+ include Zermelo::Records::RedisRecord
37
+
38
+ define_attributes :name => :string,
39
+ :important => :boolean
40
+
41
+ index_by :important
42
+
43
+ belongs_to :example, :class_name => 'Zermelo::RedisExample', :inverse_of => :children
44
+
45
+ validates :name, :presence => true
46
+ end
47
+
48
+ class RedisExampleDatum
49
+ include Zermelo::Records::RedisRecord
50
+
51
+ define_attributes :timestamp => :timestamp,
52
+ :summary => :string,
53
+ :emotion => :string
54
+
55
+ belongs_to :example, :class_name => 'Zermelo::RedisExample', :inverse_of => :data
56
+
57
+ index_by :emotion
58
+
59
+ validates :timestamp, :presence => true
60
+ end
61
+
62
+ class Template
63
+ include Zermelo::Records::RedisRecord
64
+
65
+ define_attributes :name => :string
66
+
67
+ has_and_belongs_to_many :examples, :class_name => 'Zermelo::RedisExample',
68
+ :inverse_of => :templates
69
+
70
+ validates :name, :presence => true
71
+ end
72
+ end
73
+
74
+ let(:redis) { Zermelo.redis }
75
+
76
+ def create_example(attrs = {})
77
+ redis.hmset("redis_example:#{attrs[:id]}:attrs",
78
+ {'name' => attrs[:name], 'email' => attrs[:email], 'active' => attrs[:active]}.to_a.flatten)
79
+ redis.sadd("redis_example::indices:by_active:boolean:#{!!attrs[:active]}", attrs[:id])
80
+ name = attrs[:name].gsub(/%/, '%%').gsub(/ /, '%20').gsub(/:/, '%3A')
81
+ redis.hset('redis_example::indices:by_name', "string:#{name}", attrs[:id])
82
+ redis.sadd('redis_example::attrs:ids', attrs[:id])
83
+ end
84
+
85
+ it "is invalid without a name" do
86
+ example = Zermelo::RedisExample.new(:id => '1', :email => 'jsmith@example.com')
87
+ expect(example).not_to be_valid
88
+
89
+ errs = example.errors
90
+ expect(errs).not_to be_nil
91
+ expect(errs[:name]).to eq(["can't be blank"])
92
+ end
93
+
94
+ it "adds a record's attributes to redis" do
95
+ example = Zermelo::RedisExample.new(:id => '1', :name => 'John Smith',
96
+ :email => 'jsmith@example.com', :active => true)
97
+ expect(example).to be_valid
98
+ expect(example.save).to be_truthy
99
+
100
+ expect(redis.keys('*')).to match_array(['redis_example::attrs:ids',
101
+ 'redis_example:1:attrs',
102
+ 'redis_example::indices:by_name',
103
+ 'redis_example::indices:by_active:boolean:true'])
104
+ expect(redis.smembers('redis_example::attrs:ids')).to eq(['1'])
105
+ expect(redis.hgetall('redis_example:1:attrs')).to eq(
106
+ {'name' => 'John Smith', 'email' => 'jsmith@example.com', 'active' => 'true'}
107
+ )
108
+ expect(redis.hgetall('redis_example::indices:by_name')).to eq({'string:John%20Smith' => '1'})
109
+ expect(redis.smembers('redis_example::indices:by_active:boolean:true')).to eq(
110
+ ['1']
111
+ )
112
+ end
113
+
114
+ it "finds a record by id in redis" do
115
+ create_example(:id => '8', :name => 'John Jones',
116
+ :email => 'jjones@example.com', :active => 'true')
117
+
118
+ example = Zermelo::RedisExample.find_by_id('8')
119
+ expect(example).not_to be_nil
120
+ expect(example.id).to eq('8')
121
+ expect(example.name).to eq('John Jones')
122
+ expect(example.email).to eq('jjones@example.com')
123
+ end
124
+
125
+ it "finds records by a uniquely indexed value in redis" do
126
+ create_example(:id => '8', :name => 'John Jones',
127
+ :email => 'jjones@example.com', :active => 'true')
128
+
129
+ examples = Zermelo::RedisExample.intersect(:name => 'John Jones').all
130
+ expect(examples).not_to be_nil
131
+ expect(examples).to be_an(Array)
132
+ expect(examples.size).to eq(1)
133
+ example = examples.first
134
+ expect(example.id).to eq('8')
135
+ expect(example.name).to eq('John Jones')
136
+ expect(example.email).to eq('jjones@example.com')
137
+ end
138
+
139
+ it 'finds records by regex match against an indexed value in redis'
140
+
141
+ it 'finds records by regex match against a uniquely indexed value in redis' do
142
+ create_example(:id => '8', :name => 'John Jones',
143
+ :email => 'jjones@example.com', :active => 'true')
144
+
145
+ examples = Zermelo::RedisExample.intersect(:name => /hn Jones/).all
146
+ expect(examples).not_to be_nil
147
+ expect(examples).to be_an(Array)
148
+ expect(examples.size).to eq(1)
149
+ example = examples.first
150
+ expect(example.id).to eq('8')
151
+ expect(example.name).to eq('John Jones')
152
+ expect(example.email).to eq('jjones@example.com')
153
+ end
154
+
155
+ it 'cannot find records by regex match against non-string values' do
156
+ create_example(:id => '8', :name => 'John Jones',
157
+ :email => 'jjones@example.com', :active => true)
158
+ create_example(:id => '9', :name => 'James Brown',
159
+ :email => 'jbrown@example.com', :active => false)
160
+
161
+ expect {
162
+ Zermelo::RedisExample.intersect(:active => /alse/).all
163
+ }.to raise_error
164
+ end
165
+
166
+ it "updates a record's attributes in redis" do
167
+ create_example(:id => '8', :name => 'John Jones',
168
+ :email => 'jjones@example.com', :active => 'true')
169
+
170
+ example = Zermelo::RedisExample.find_by_id('8')
171
+ example.name = 'Jane Janes'
172
+ example.email = 'jjanes@example.com'
173
+ expect(example.save).to be_truthy
174
+
175
+ expect(redis.keys('*')).to match_array(['redis_example::attrs:ids',
176
+ 'redis_example:8:attrs',
177
+ 'redis_example::indices:by_name',
178
+ 'redis_example::indices:by_active:boolean:true'])
179
+ expect(redis.smembers('redis_example::attrs:ids')).to eq(['8'])
180
+ expect(redis.hgetall('redis_example:8:attrs')).to eq(
181
+ {'name' => 'Jane Janes', 'email' => 'jjanes@example.com', 'active' => 'true'}
182
+ )
183
+ expect(redis.smembers('redis_example::indices:by_active:boolean:true')).to eq(
184
+ ['8']
185
+ )
186
+ end
187
+
188
+ it "deletes a record's attributes from redis" do
189
+ create_example(:id => '8', :name => 'John Jones',
190
+ :email => 'jjones@example.com', :active => 'true')
191
+
192
+ expect(redis.keys('*')).to match_array(['redis_example::attrs:ids',
193
+ 'redis_example:8:attrs',
194
+ 'redis_example::indices:by_name',
195
+ 'redis_example::indices:by_active:boolean:true'])
196
+
197
+ example = Zermelo::RedisExample.find_by_id('8')
198
+ example.destroy
199
+
200
+ expect(redis.keys('*')).to eq([])
201
+ end
202
+
203
+ it "resets changed state on refresh" do
204
+ create_example(:id => '8', :name => 'John Jones',
205
+ :email => 'jjones@example.com', :active => 'true')
206
+ example = Zermelo::RedisExample.find_by_id('8')
207
+
208
+ example.name = "King Henry VIII"
209
+ expect(example.changed).to include('name')
210
+ expect(example.changes).to eq({'name' => ['John Jones', 'King Henry VIII']})
211
+
212
+ example.refresh
213
+ expect(example.changed).to be_empty
214
+ expect(example.changes).to be_empty
215
+ end
216
+
217
+ it "stores a string as an attribute value"
218
+ it "stores an integer as an attribute value"
219
+ it "stores a timestamp as an attribute value"
220
+ it "stores a boolean as an attribute value"
221
+
222
+ it "stores a list as an attribute value"
223
+ it "stores a set as an attribute value"
224
+ it "stores a hash as an attribute value"
225
+
226
+ context 'pagination' do
227
+
228
+ before do
229
+ create_example(:id => '1', :name => 'mno')
230
+ create_example(:id => '2', :name => 'abc')
231
+ create_example(:id => '3', :name => 'jkl')
232
+ create_example(:id => '4', :name => 'ghi')
233
+ create_example(:id => '5', :name => 'def')
234
+ end
235
+
236
+ it "returns paginated query responses" do
237
+ expect(Zermelo::RedisExample.sort(:id).page(1, :per_page => 3).map(&:id)).to eq(['1','2', '3'])
238
+ expect(Zermelo::RedisExample.sort(:id).page(2, :per_page => 2).map(&:id)).to eq(['3','4'])
239
+ expect(Zermelo::RedisExample.sort(:id).page(3, :per_page => 2).map(&:id)).to eq(['5'])
240
+ expect(Zermelo::RedisExample.sort(:id).page(3, :per_page => 3).map(&:id)).to eq([])
241
+
242
+ expect(Zermelo::RedisExample.sort(:name).page(1, :per_page => 3).map(&:id)).to eq(['2','5', '4'])
243
+ expect(Zermelo::RedisExample.sort(:name).page(2, :per_page => 2).map(&:id)).to eq(['4','3'])
244
+ expect(Zermelo::RedisExample.sort(:name).page(3, :per_page => 2).map(&:id)).to eq(['1'])
245
+ expect(Zermelo::RedisExample.sort(:name).page(3, :per_page => 3).map(&:id)).to eq([])
246
+ end
247
+
248
+ end
249
+
250
+ context 'filters' do
251
+
252
+ let(:active) {
253
+ create_example(:id => '8', :name => 'John Jones',
254
+ :email => 'jjones@example.com', :active => true)
255
+ }
256
+
257
+ let(:inactive) {
258
+ create_example(:id => '9', :name => 'James Brown',
259
+ :email => 'jbrown@example.com', :active => false)
260
+ }
261
+
262
+ before do
263
+ active; inactive
264
+ end
265
+
266
+ it 'can append to a filter chain fragment more than once' do
267
+ inter = Zermelo::RedisExample.intersect(:active => true)
268
+ expect(inter.ids).to eq(['8'])
269
+
270
+ union = inter.union(:name => 'James Brown')
271
+ expect(union.ids).to eq(['8', '9'])
272
+
273
+ diff = inter.diff(:id => ['8'])
274
+ expect(diff.ids).to eq([])
275
+ end
276
+
277
+ it "filters all class records by indexed attribute values" do
278
+ example = Zermelo::RedisExample.intersect(:active => true).all
279
+ expect(example).not_to be_nil
280
+ expect(example).to be_an(Array)
281
+ expect(example.size).to eq(1)
282
+ expect(example.map(&:id)).to eq(['8'])
283
+ end
284
+
285
+ it 'filters by id attribute values' do
286
+ example = Zermelo::RedisExample.intersect(:id => '9').all
287
+ expect(example).not_to be_nil
288
+ expect(example).to be_an(Array)
289
+ expect(example.size).to eq(1)
290
+ expect(example.map(&:id)).to eq(['9'])
291
+ end
292
+
293
+ it 'supports sequential intersection and union operations' do
294
+ examples = Zermelo::RedisExample.intersect(:active => true).union(:active => false).all
295
+ expect(examples).not_to be_nil
296
+ expect(examples).to be_an(Array)
297
+ expect(examples.size).to eq(2)
298
+ expect(examples.map(&:id)).to match_array(['8', '9'])
299
+ end
300
+
301
+ it "ANDs multiple union arguments, not ORs them" do
302
+ create_example(:id => '10', :name => 'Jay Johns',
303
+ :email => 'jjohns@example.com', :active => true)
304
+ examples = Zermelo::RedisExample.intersect(:id => ['8']).union(:id => ['9', '10'], :active => true).all
305
+ expect(examples).not_to be_nil
306
+ expect(examples).to be_an(Array)
307
+ expect(examples.size).to eq(2)
308
+ expect(examples.map(&:id)).to match_array(['8', '10'])
309
+ end
310
+
311
+ it 'supports a regex as argument in union after intersect' do
312
+ create_example(:id => '10', :name => 'Jay Johns',
313
+ :email => 'jjohns@example.com', :active => true)
314
+ examples = Zermelo::RedisExample.intersect(:id => ['8']).union(:id => ['9', '10'], :name => [nil, /^Jam/]).all
315
+ expect(examples).not_to be_nil
316
+ expect(examples).to be_an(Array)
317
+ expect(examples.size).to eq(2)
318
+ expect(examples.map(&:id)).to match_array(['8', '9'])
319
+ end
320
+
321
+ it 'allows intersection operations across multiple values for an attribute' do
322
+ create_example(:id => '10', :name => 'Jay Johns',
323
+ :email => 'jjohns@example.com', :active => true)
324
+
325
+ examples = Zermelo::RedisExample.intersect(:name => ['Jay Johns', 'James Brown']).all
326
+ expect(examples).not_to be_nil
327
+ expect(examples).to be_an(Array)
328
+ expect(examples.size).to eq(2)
329
+ expect(examples.map(&:id)).to match_array(['9', '10'])
330
+ end
331
+
332
+ it 'allows union operations across multiple values for an attribute' do
333
+ create_example(:id => '10', :name => 'Jay Johns',
334
+ :email => 'jjohns@example.com', :active => true)
335
+
336
+ examples = Zermelo::RedisExample.intersect(:active => false).union(:name => ['Jay Johns', 'James Brown']).all
337
+ expect(examples).not_to be_nil
338
+ expect(examples).to be_an(Array)
339
+ expect(examples.size).to eq(2)
340
+ expect(examples.map(&:id)).to match_array(['9', '10'])
341
+ end
342
+
343
+ it 'filters by multiple id attribute values' do
344
+ create_example(:id => '10', :name => 'Jay Johns',
345
+ :email => 'jjohns@example.com', :active => true)
346
+
347
+ example = Zermelo::RedisExample.intersect(:id => ['8', '10']).all
348
+ expect(example).not_to be_nil
349
+ expect(example).to be_an(Array)
350
+ expect(example.size).to eq(2)
351
+ expect(example.map(&:id)).to eq(['8', '10'])
352
+ end
353
+
354
+ it 'excludes particular records' do
355
+ example = Zermelo::RedisExample.diff(:active => true).all
356
+ expect(example).not_to be_nil
357
+ expect(example).to be_an(Array)
358
+ expect(example.size).to eq(1)
359
+ expect(example.map(&:id)).to eq(['9'])
360
+ end
361
+
362
+ it 'sorts records by an attribute' do
363
+ example = Zermelo::RedisExample.sort(:name, :order => 'alpha').all
364
+ expect(example).not_to be_nil
365
+ expect(example).to be_an(Array)
366
+ expect(example.size).to eq(2)
367
+ expect(example.map(&:id)).to eq(['9', '8'])
368
+ end
369
+
370
+ end
371
+
372
+
373
+ context "has_many" do
374
+
375
+ def create_child(parent, attrs = {})
376
+ redis.sadd("redis_example:#{parent.id}:assocs:children_ids", attrs[:id]) unless parent.nil?
377
+
378
+ redis.hmset("redis_example_child:#{attrs[:id]}:attrs",
379
+ {'name' => attrs[:name], 'important' => !!attrs[:important]}.to_a.flatten)
380
+
381
+ redis.hmset("redis_example_child:#{attrs[:id]}:assocs:belongs_to",
382
+ {'example_id' => parent.id}.to_a.flatten) unless parent.nil?
383
+
384
+ redis.sadd("redis_example_child::indices:by_important:boolean:#{!!attrs[:important]}", attrs[:id])
385
+
386
+ redis.sadd('redis_example_child::attrs:ids', attrs[:id])
387
+ end
388
+
389
+ it "sets a parent/child has_many relationship between two records in redis" do
390
+ create_example(:id => '8', :name => 'John Jones',
391
+ :email => 'jjones@example.com', :active => 'true')
392
+
393
+ child = Zermelo::RedisExampleChild.new(:id => '3', :name => 'Abel Tasman')
394
+ expect(child.save).to be_truthy
395
+
396
+ example = Zermelo::RedisExample.find_by_id('8')
397
+ example.children << child
398
+
399
+ expect(redis.keys('*')).to match_array(['redis_example::attrs:ids',
400
+ 'redis_example::indices:by_name',
401
+ 'redis_example::indices:by_active:boolean:true',
402
+ 'redis_example:8:attrs',
403
+ 'redis_example:8:assocs:children_ids',
404
+ 'redis_example_child::attrs:ids',
405
+ 'redis_example_child::indices:by_important:null:null',
406
+ 'redis_example_child:3:attrs',
407
+ 'redis_example_child:3:assocs:belongs_to'])
408
+
409
+ expect(redis.smembers('redis_example::attrs:ids')).to eq(['8'])
410
+ expect(redis.smembers('redis_example::indices:by_active:boolean:true')).to eq(
411
+ ['8']
412
+ )
413
+ expect(redis.hgetall('redis_example:8:attrs')).to eq(
414
+ {'name' => 'John Jones', 'email' => 'jjones@example.com', 'active' => 'true'}
415
+ )
416
+ expect(redis.smembers('redis_example:8:assocs:children_ids')).to eq(['3'])
417
+
418
+ expect(redis.smembers('redis_example_child::attrs:ids')).to eq(['3'])
419
+ expect(redis.hgetall('redis_example_child:3:attrs')).to eq(
420
+ {'name' => 'Abel Tasman'}
421
+ )
422
+ end
423
+
424
+ it "loads a child from a parent's has_many relationship" do
425
+ create_example(:id => '8', :name => 'John Jones',
426
+ :email => 'jjones@example.com', :active => 'true')
427
+ example = Zermelo::RedisExample.find_by_id('8')
428
+ create_child(example, :id => '3', :name => 'Abel Tasman')
429
+
430
+ children = example.children.all
431
+
432
+ expect(children).to be_an(Array)
433
+ expect(children.size).to eq(1)
434
+ child = children.first
435
+ expect(child).to be_a(Zermelo::RedisExampleChild)
436
+ expect(child.name).to eq('Abel Tasman')
437
+ end
438
+
439
+ it "loads a parent from a child's belongs_to relationship" do
440
+ create_example(:id => '8', :name => 'John Jones',
441
+ :email => 'jjones@example.com', :active => 'true')
442
+ example = Zermelo::RedisExample.find_by_id('8')
443
+ create_child(example, :id => '3', :name => 'Abel Tasman')
444
+ child = Zermelo::RedisExampleChild.find_by_id('3')
445
+
446
+ other_example = child.example
447
+ expect(other_example).not_to be_nil
448
+ expect(other_example).to be_a(Zermelo::RedisExample)
449
+ expect(other_example.name).to eq('John Jones')
450
+ end
451
+
452
+ it "removes a parent/child has_many relationship between two records in redis" do
453
+ create_example(:id => '8', :name => 'John Jones',
454
+ :email => 'jjones@example.com', :active => 'true')
455
+ example = Zermelo::RedisExample.find_by_id('8')
456
+
457
+ create_child(example, :id => '3', :name => 'Abel Tasman')
458
+ child = Zermelo::RedisExampleChild.find_by_id('3')
459
+
460
+ expect(redis.smembers('redis_example_child::attrs:ids')).to eq(['3'])
461
+ expect(redis.smembers('redis_example:8:assocs:children_ids')).to eq(['3'])
462
+
463
+ example.children.delete(child)
464
+
465
+ expect(redis.smembers('redis_example_child::attrs:ids')).to eq(['3']) # child not deleted
466
+ expect(redis.smembers('redis_example:8:assocs:children_ids')).to eq([]) # but association is
467
+ end
468
+
469
+ it "filters has_many records by indexed attribute values" do
470
+ create_example(:id => '8', :name => 'John Jones',
471
+ :email => 'jjones@example.com', :active => 'true')
472
+ example = Zermelo::RedisExample.find_by_id('8')
473
+
474
+ create_child(example, :id => '3', :name => 'Martin Luther King', :important => true)
475
+ create_child(example, :id => '4', :name => 'Julius Caesar', :important => true)
476
+ create_child(example, :id => '5', :name => 'John Smith', :important => false)
477
+
478
+ important_kids = example.children.intersect(:important => true).all
479
+ expect(important_kids).not_to be_nil
480
+ expect(important_kids).to be_an(Array)
481
+ expect(important_kids.size).to eq(2)
482
+ expect(important_kids.map(&:id)).to match_array(['3', '4'])
483
+ end
484
+
485
+ it "filters has_many records by intersecting ids" do
486
+ create_example(:id => '8', :name => 'John Jones',
487
+ :email => 'jjones@example.com', :active => 'true')
488
+ example = Zermelo::RedisExample.find_by_id('8')
489
+
490
+ create_child(example, :id => '3', :name => 'Martin Luther King', :important => true)
491
+ create_child(example, :id => '4', :name => 'Julius Caesar', :important => true)
492
+ create_child(example, :id => '5', :name => 'John Smith', :important => false)
493
+
494
+ important_kids = example.children.intersect(:important => true, :id => ['4', '5']).all
495
+ expect(important_kids).not_to be_nil
496
+ expect(important_kids).to be_an(Array)
497
+ expect(important_kids.size).to eq(1)
498
+ expect(important_kids.map(&:id)).to match_array(['4'])
499
+ end
500
+
501
+ it "checks whether a record id exists through a has_many filter" do
502
+ create_example(:id => '8', :name => 'John Jones',
503
+ :email => 'jjones@example.com', :active => 'true')
504
+ example = Zermelo::RedisExample.find_by_id('8')
505
+
506
+ create_child(example, :id => '3', :name => 'Martin Luther King', :important => true)
507
+ create_child(example, :id => '4', :name => 'Julius Caesar', :important => true)
508
+ create_child(example, :id => '5', :name => 'John Smith', :important => false)
509
+
510
+ expect(example.children.intersect(:important => true).exists?('3')).to be_truthy
511
+ expect(example.children.intersect(:important => true).exists?('5')).to be_falsey
512
+ end
513
+
514
+ it "finds a record through a has_many filter" do
515
+ create_example(:id => '8', :name => 'John Jones',
516
+ :email => 'jjones@example.com', :active => 'true')
517
+ example = Zermelo::RedisExample.find_by_id('8')
518
+
519
+ create_child(example, :id => '3', :name => 'Martin Luther King', :important => true)
520
+ create_child(example, :id => '4', :name => 'Julius Caesar', :important => true)
521
+ create_child(example, :id => '5', :name => 'John Smith', :important => false)
522
+
523
+ martin = example.children.intersect(:important => true).find_by_id('3')
524
+ expect(martin).not_to be_nil
525
+ expect(martin).to be_a(Zermelo::RedisExampleChild)
526
+ expect(martin.id).to eq('3')
527
+ end
528
+
529
+ it "does not add a child if the before_add callback raises an exception" do
530
+ create_example(:id => '8', :name => 'John Jones',
531
+ :email => 'jjones@example.com', :active => 'true')
532
+ example = Zermelo::RedisExample.find_by_id('8')
533
+
534
+ create_child(nil, :id => '6', :name => 'Roger', :important => true)
535
+ child = Zermelo::RedisExampleChild.find_by_id('6')
536
+
537
+ expect(example.children).to be_empty
538
+ expect {
539
+ example.children << child
540
+ }.to raise_error
541
+ expect(example.children).to be_empty
542
+ end
543
+
544
+ it 'clears the belongs_to association when the child record is deleted' do
545
+ create_example(:id => '8', :name => 'John Jones',
546
+ :email => 'jjones@example.com', :active => 'true')
547
+ example = Zermelo::RedisExample.find_by_id('8')
548
+
549
+ time = Time.now
550
+
551
+ create_child(example, :id => '6', :name => 'Martin Luther King', :important => true)
552
+ child = Zermelo::RedisExampleChild.find_by_id('6')
553
+
554
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
555
+ 'redis_example::indices:by_name',
556
+ 'redis_example::indices:by_active:boolean:true',
557
+ 'redis_example:8:attrs',
558
+ 'redis_example:8:assocs:children_ids',
559
+ 'redis_example_child::attrs:ids',
560
+ 'redis_example_child::indices:by_important:boolean:true',
561
+ 'redis_example_child:6:attrs',
562
+ 'redis_example_child:6:assocs:belongs_to'])
563
+
564
+ child.destroy
565
+
566
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
567
+ 'redis_example::indices:by_name',
568
+ 'redis_example::indices:by_active:boolean:true',
569
+ 'redis_example:8:attrs'])
570
+ end
571
+
572
+ it "clears the belongs_to association when the parent record is deleted" do
573
+ create_example(:id => '8', :name => 'John Jones',
574
+ :email => 'jjones@example.com', :active => 'true')
575
+ example = Zermelo::RedisExample.find_by_id('8')
576
+
577
+ time = Time.now
578
+
579
+ create_child(example, :id => '6', :name => 'Martin Luther King', :important => true)
580
+ child = Zermelo::RedisExampleChild.find_by_id('6')
581
+
582
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
583
+ 'redis_example::indices:by_name',
584
+ 'redis_example::indices:by_active:boolean:true',
585
+ 'redis_example:8:attrs',
586
+ 'redis_example:8:assocs:children_ids',
587
+ 'redis_example_child::attrs:ids',
588
+ 'redis_example_child::indices:by_important:boolean:true',
589
+ 'redis_example_child:6:attrs',
590
+ 'redis_example_child:6:assocs:belongs_to'])
591
+
592
+ example.destroy
593
+
594
+ expect(redis.keys).to match_array(['redis_example_child::attrs:ids',
595
+ 'redis_example_child::indices:by_important:boolean:true',
596
+ 'redis_example_child:6:attrs'])
597
+ end
598
+
599
+ it 'returns associated ids for multiple parent ids' do
600
+ create_example(:id => '8', :name => 'John Jones',
601
+ :email => 'jjones@example.com', :active => 'true')
602
+ example_8 = Zermelo::RedisExample.find_by_id('8')
603
+
604
+ create_example(:id => '9', :name => 'Jane Johnson',
605
+ :email => 'jjohnson@example.com', :active => 'true')
606
+ example_9 = Zermelo::RedisExample.find_by_id('9')
607
+
608
+ create_example(:id => '10', :name => 'Jim Smith',
609
+ :email => 'jsmith@example.com', :active => 'true')
610
+
611
+ create_child(example_8, :id => '3', :name => 'abc', :important => false)
612
+ create_child(example_9, :id => '4', :name => 'abc', :important => false)
613
+ create_child(example_9, :id => '5', :name => 'abc', :important => false)
614
+
615
+ assoc_ids = Zermelo::RedisExample.intersect(:id => [ '8', '9', '10']).
616
+ associated_ids_for(:children)
617
+ expect(assoc_ids).to eq('8' => Set.new(['3']),
618
+ '9' => Set.new(['4', '5']),
619
+ '10' => Set.new())
620
+
621
+ assoc_parent_ids = Zermelo::RedisExampleChild.intersect(:id => ['3', '4', '5']).
622
+ associated_ids_for(:example)
623
+ expect(assoc_parent_ids).to eq('3' => '8',
624
+ '4' => '9',
625
+ '5' => '9')
626
+ end
627
+
628
+ end
629
+
630
+ context "has_sorted_set" do
631
+
632
+ def create_datum(parent, attrs = {})
633
+ redis.zadd("redis_example:#{parent.id}:assocs:data_ids", attrs[:timestamp].to_i.to_f, attrs[:id])
634
+
635
+ redis.hmset("redis_example_datum:#{attrs[:id]}:attrs",
636
+ {'summary' => attrs[:summary], 'timestamp' => attrs[:timestamp].to_i.to_f,
637
+ 'emotion' => attrs[:emotion]}.to_a.flatten)
638
+
639
+ redis.sadd("redis_example_datum::indices:by_emotion:string:#{attrs[:emotion]}", attrs[:id])
640
+ redis.hset("redis_example_datum:#{attrs[:id]}:assocs:belongs_to", 'example_id', parent.id)
641
+
642
+ redis.sadd('redis_example_datum::attrs:ids', attrs[:id])
643
+ end
644
+
645
+ it "sets a parent/child has_sorted_set relationship between two records in redis" do
646
+ create_example(:id => '8', :name => 'John Jones',
647
+ :email => 'jjones@example.com', :active => 'true')
648
+
649
+ time = Time.now
650
+
651
+ data = Zermelo::RedisExampleDatum.new(:id => '4', :timestamp => time,
652
+ :summary => "hello!")
653
+ expect(data.save).to be_truthy
654
+
655
+ example = Zermelo::RedisExample.find_by_id('8')
656
+ example.data << data
657
+
658
+ expect(redis.keys('*')).to match_array(['redis_example::attrs:ids',
659
+ 'redis_example::indices:by_name',
660
+ 'redis_example::indices:by_active:boolean:true',
661
+ 'redis_example:8:attrs',
662
+ 'redis_example:8:assocs:data_ids',
663
+ 'redis_example_datum::attrs:ids',
664
+ 'redis_example_datum::indices:by_emotion:null:null',
665
+ 'redis_example_datum:4:attrs',
666
+ 'redis_example_datum:4:assocs:belongs_to'])
667
+
668
+ expect(redis.smembers('redis_example_datum::attrs:ids')).to eq(['4'])
669
+ expect(redis.hgetall('redis_example_datum:4:attrs')).to eq(
670
+ {'summary' => 'hello!', 'timestamp' => time.to_f.to_s}
671
+ )
672
+ expect(redis.hgetall('redis_example_datum:4:assocs:belongs_to')).to eq(
673
+ {'example_id' => '8'}
674
+ )
675
+
676
+ result = redis.zrange('redis_example:8:assocs:data_ids', 0, -1,
677
+ :with_scores => true) # .should == [['4', time.to_f]]
678
+ expect(result.size).to eq(1)
679
+ expect(result.first.first).to eq('4')
680
+ expect(result.first.last).to be_within(0.001).of(time.to_f)
681
+ end
682
+
683
+ it "loads a child from a parent's has_sorted_set relationship" do
684
+ create_example(:id => '8', :name => 'John Jones',
685
+ :email => 'jjones@example.com', :active => 'true')
686
+ example = Zermelo::RedisExample.find_by_id('8')
687
+
688
+ time = Time.now
689
+
690
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time)
691
+ datum = Zermelo::RedisExampleDatum.find_by_id('4')
692
+
693
+ data = example.data.all
694
+
695
+ expect(data).to be_an(Array)
696
+ expect(data.size).to eq(1)
697
+ datum = data.first
698
+ expect(datum).to be_a(Zermelo::RedisExampleDatum)
699
+ expect(datum.summary).to eq('well then')
700
+ expect(datum.timestamp).to be_within(1).of(time) # ignore fractional differences
701
+ end
702
+
703
+ it "removes a parent/child has_sorted_set relationship between two records in redis" do
704
+ create_example(:id => '8', :name => 'John Jones',
705
+ :email => 'jjones@example.com', :active => 'true')
706
+ example = Zermelo::RedisExample.find_by_id('8')
707
+
708
+ time = Time.now
709
+
710
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time)
711
+ datum = Zermelo::RedisExampleDatum.find_by_id('4')
712
+
713
+ expect(redis.smembers('redis_example_datum::attrs:ids')).to eq(['4'])
714
+ expect(redis.zrange('redis_example:8:assocs:data_ids', 0, -1)).to eq(['4'])
715
+
716
+ example.data.delete(datum)
717
+
718
+ expect(redis.smembers('redis_example_datum::attrs:ids')).to eq(['4']) # child not deleted
719
+ expect(redis.zrange('redis_example:8:assocs.data_ids', 0, -1)).to eq([]) # but association is
720
+ end
721
+
722
+ it "filters has_sorted_set records by indexed attribute values" do
723
+ create_example(:id => '8', :name => 'John Jones',
724
+ :email => 'jjones@example.com', :active => 'true')
725
+ example = Zermelo::RedisExample.find_by_id('8')
726
+
727
+ time = Time.now
728
+
729
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
730
+ :emotion => 'upset')
731
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
732
+ :emotion => 'happy')
733
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
734
+ :emotion => 'upset')
735
+
736
+ upset_data = example.data.intersect(:emotion => 'upset').all
737
+ expect(upset_data).not_to be_nil
738
+ expect(upset_data).to be_an(Array)
739
+ expect(upset_data.size).to eq(2)
740
+ expect(upset_data.map(&:id)).to eq(['4', '6'])
741
+ end
742
+
743
+
744
+ it "filters has_sorted_set records by indexed attribute values with a regex search" do
745
+ create_example(:id => '8', :name => 'John Jones',
746
+ :email => 'jjones@example.com', :active => 'true')
747
+ example = Zermelo::RedisExample.find_by_id('8')
748
+
749
+ time = Time.now
750
+
751
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
752
+ :emotion => 'upset')
753
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
754
+ :emotion => 'happy')
755
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
756
+ :emotion => 'upset')
757
+
758
+ upset_data = example.data.intersect(:emotion => /^ups/).all
759
+ expect(upset_data).not_to be_nil
760
+ expect(upset_data).to be_an(Array)
761
+ expect(upset_data.size).to eq(2)
762
+ expect(upset_data.map(&:id)).to eq(['4', '6'])
763
+ end
764
+
765
+ it "retrieves a subset of a sorted set by index" do
766
+ create_example(:id => '8', :name => 'John Jones',
767
+ :email => 'jjones@example.com', :active => 'true')
768
+ example = Zermelo::RedisExample.find_by_id('8')
769
+
770
+ time = Time.now
771
+
772
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
773
+ :emotion => 'upset')
774
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
775
+ :emotion => 'happy')
776
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
777
+ :emotion => 'upset')
778
+
779
+ data = example.data.intersect_range(0, 1).all
780
+ expect(data).not_to be_nil
781
+ expect(data).to be_an(Array)
782
+ expect(data.size).to eq(2)
783
+ expect(data.map(&:id)).to eq(['4', '5'])
784
+ end
785
+
786
+ it "retrieves a reversed subset of a sorted set by index" do
787
+ create_example(:id => '8', :name => 'John Jones',
788
+ :email => 'jjones@example.com', :active => 'true')
789
+ example = Zermelo::RedisExample.find_by_id('8')
790
+
791
+ time = Time.now
792
+
793
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time.to_i,
794
+ :emotion => 'upset')
795
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
796
+ :emotion => 'happy')
797
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
798
+ :emotion => 'upset')
799
+
800
+ data = example.data.intersect_range(0, 1, :desc => true).all
801
+
802
+ expect(data).not_to be_nil
803
+ expect(data).to be_an(Array)
804
+ expect(data.size).to eq(2)
805
+ expect(data.map(&:id)).to eq(['6', '5'])
806
+ end
807
+
808
+ it "retrieves a subset of a sorted set by score" do
809
+ create_example(:id => '8', :name => 'John Jones',
810
+ :email => 'jjones@example.com', :active => 'true')
811
+ example = Zermelo::RedisExample.find_by_id('8')
812
+
813
+ time = Time.now
814
+
815
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
816
+ :emotion => 'upset')
817
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
818
+ :emotion => 'happy')
819
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
820
+ :emotion => 'upset')
821
+
822
+ data = example.data.intersect_range(time.to_i - 1, time.to_i + 15, :by_score => true).all
823
+ expect(data).not_to be_nil
824
+ expect(data).to be_an(Array)
825
+ expect(data.size).to eq(2)
826
+ expect(data.map(&:id)).to eq(['4', '5'])
827
+ end
828
+
829
+ it "retrieves a reversed subset of a sorted set by score" do
830
+ create_example(:id => '8', :name => 'John Jones',
831
+ :email => 'jjones@example.com', :active => 'true')
832
+ example = Zermelo::RedisExample.find_by_id('8')
833
+
834
+ time = Time.now
835
+
836
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
837
+ :emotion => 'upset')
838
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
839
+ :emotion => 'happy')
840
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
841
+ :emotion => 'upset')
842
+
843
+ data = example.data.intersect_range(time.to_i - 1, time.to_i + 15,
844
+ :desc => true, :by_score => true).all
845
+ expect(data).not_to be_nil
846
+ expect(data).to be_an(Array)
847
+ expect(data.size).to eq(2)
848
+ expect(data.map(&:id)).to eq(['5', '4'])
849
+ end
850
+
851
+ it "checks whether a record exists through a has_sorted_set filter" do
852
+ create_example(:id => '8', :name => 'John Jones',
853
+ :email => 'jjones@example.com', :active => 'true')
854
+ example = Zermelo::RedisExample.find_by_id('8')
855
+
856
+ time = Time.now
857
+
858
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
859
+ :emotion => 'upset')
860
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
861
+ :emotion => 'happy')
862
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
863
+ :emotion => 'upset')
864
+
865
+ expect(example.data.intersect(:emotion => 'upset').exists?('4')).to be_truthy
866
+ expect(example.data.intersect(:emotion => 'upset').exists?('5')).to be_falsey
867
+ end
868
+
869
+ it "retrieves the union of a sorted set by index"
870
+ it "retrieves a reversed union of a sorted set by index"
871
+
872
+ it "retrieves the union of a sorted set by score"
873
+ it "retrieves a reversed union of a sorted set by score"
874
+
875
+ it "retrieves the exclusion of a sorted set by index" do
876
+ create_example(:id => '8', :name => 'John Jones',
877
+ :email => 'jjones@example.com', :active => 'true')
878
+ example = Zermelo::RedisExample.find_by_id('8')
879
+
880
+ time = Time.now
881
+
882
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
883
+ :emotion => 'upset')
884
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
885
+ :emotion => 'happy')
886
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
887
+ :emotion => 'upset')
888
+
889
+ data = example.data.diff_range(0, 1).all
890
+ expect(data).not_to be_nil
891
+ expect(data).to be_an(Array)
892
+ expect(data.size).to eq(1)
893
+ expect(data.map(&:id)).to eq(['6'])
894
+ end
895
+
896
+ it "retrieves a reversed exclusion of a sorted set by index" do
897
+ create_example(:id => '8', :name => 'John Jones',
898
+ :email => 'jjones@example.com', :active => 'true')
899
+ example = Zermelo::RedisExample.find_by_id('8')
900
+
901
+ time = Time.now
902
+
903
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
904
+ :emotion => 'upset')
905
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
906
+ :emotion => 'happy')
907
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
908
+ :emotion => 'upset')
909
+
910
+ data = example.data.diff_range(0, 0, :desc => true).all
911
+ expect(data).not_to be_nil
912
+ expect(data).to be_an(Array)
913
+ expect(data.size).to eq(2)
914
+ expect(data.map(&:id)).to eq(['5', '4'])
915
+ end
916
+
917
+ it "retrieves the exclusion of a sorted set by score" do
918
+ create_example(:id => '8', :name => 'John Jones',
919
+ :email => 'jjones@example.com', :active => 'true')
920
+ example = Zermelo::RedisExample.find_by_id('8')
921
+
922
+ time = Time.now
923
+
924
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
925
+ :emotion => 'upset')
926
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
927
+ :emotion => 'happy')
928
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
929
+ :emotion => 'upset')
930
+
931
+ data = example.data.diff_range(time.to_i - 1, time.to_i + 15, :by_score => true).all
932
+ expect(data).not_to be_nil
933
+ expect(data).to be_an(Array)
934
+ expect(data.size).to eq(1)
935
+ expect(data.map(&:id)).to eq(['6'])
936
+ end
937
+
938
+ it "retrieves a reversed exclusion of a sorted set by score" do
939
+ create_example(:id => '8', :name => 'John Jones',
940
+ :email => 'jjones@example.com', :active => 'true')
941
+ example = Zermelo::RedisExample.find_by_id('8')
942
+
943
+ time = Time.now
944
+
945
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
946
+ :emotion => 'upset')
947
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
948
+ :emotion => 'happy')
949
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
950
+ :emotion => 'upset')
951
+
952
+ data = example.data.diff_range(time.to_i - 1, time.to_i + 8, :by_score => true, :desc => true).all
953
+ expect(data).not_to be_nil
954
+ expect(data).to be_an(Array)
955
+ expect(data.size).to eq(2)
956
+ expect(data.map(&:id)).to eq(['6', '5'])
957
+ end
958
+
959
+ it "finds a record through a has_sorted_set filter" do
960
+ create_example(:id => '8', :name => 'John Jones',
961
+ :email => 'jjones@example.com', :active => 'true')
962
+ example = Zermelo::RedisExample.find_by_id('8')
963
+
964
+ time = Time.now
965
+
966
+ create_datum(example, :id => '4', :summary => 'well then', :timestamp => time,
967
+ :emotion => 'upset')
968
+ create_datum(example, :id => '5', :summary => 'ok', :timestamp => time.to_i + 10,
969
+ :emotion => 'happy')
970
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
971
+ :emotion => 'upset')
972
+
973
+ wellthen = upset_data = example.data.intersect(:emotion => 'upset').find_by_id('4')
974
+ expect(wellthen).not_to be_nil
975
+ expect(wellthen).to be_a(Zermelo::RedisExampleDatum)
976
+ expect(wellthen.id).to eq('4')
977
+ end
978
+
979
+ it 'clears the belongs_to association when the child record is deleted' do
980
+ create_example(:id => '8', :name => 'John Jones',
981
+ :email => 'jjones@example.com', :active => 'true')
982
+ example = Zermelo::RedisExample.find_by_id('8')
983
+
984
+ time = Time.now
985
+
986
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
987
+ :emotion => 'upset')
988
+ datum = Zermelo::RedisExampleDatum.find_by_id('6')
989
+
990
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
991
+ 'redis_example::indices:by_name',
992
+ 'redis_example::indices:by_active:boolean:true',
993
+ 'redis_example:8:attrs',
994
+ 'redis_example:8:assocs:data_ids',
995
+ 'redis_example_datum::attrs:ids',
996
+ 'redis_example_datum::indices:by_emotion:string:upset',
997
+ 'redis_example_datum:6:attrs',
998
+ 'redis_example_datum:6:assocs:belongs_to'])
999
+
1000
+ datum.destroy
1001
+
1002
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
1003
+ 'redis_example::indices:by_name',
1004
+ 'redis_example::indices:by_active:boolean:true',
1005
+ 'redis_example:8:attrs'])
1006
+ end
1007
+
1008
+ it "clears the belongs_to association when the parent record is deleted" do
1009
+ create_example(:id => '8', :name => 'John Jones',
1010
+ :email => 'jjones@example.com', :active => 'true')
1011
+ example = Zermelo::RedisExample.find_by_id('8')
1012
+
1013
+ time = Time.now
1014
+
1015
+ create_datum(example, :id => '6', :summary => 'aaargh', :timestamp => time.to_i + 20,
1016
+ :emotion => 'upset')
1017
+
1018
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
1019
+ 'redis_example::indices:by_name',
1020
+ 'redis_example::indices:by_active:boolean:true',
1021
+ 'redis_example:8:attrs',
1022
+ 'redis_example:8:assocs:data_ids',
1023
+ 'redis_example_datum::attrs:ids',
1024
+ 'redis_example_datum::indices:by_emotion:string:upset',
1025
+ 'redis_example_datum:6:attrs',
1026
+ 'redis_example_datum:6:assocs:belongs_to'])
1027
+
1028
+ example.destroy
1029
+
1030
+ expect(redis.keys).to match_array(['redis_example_datum::attrs:ids',
1031
+ 'redis_example_datum::indices:by_emotion:string:upset',
1032
+ 'redis_example_datum:6:attrs'])
1033
+ end
1034
+
1035
+ it 'returns associated ids for multiple parent ids' do
1036
+ create_example(:id => '8', :name => 'John Jones',
1037
+ :email => 'jjones@example.com', :active => 'true')
1038
+ example_8 = Zermelo::RedisExample.find_by_id('8')
1039
+
1040
+ create_example(:id => '9', :name => 'Jane Johnson',
1041
+ :email => 'jjohnson@example.com', :active => 'true')
1042
+
1043
+ create_example(:id => '10', :name => 'Jim Smith',
1044
+ :email => 'jsmith@example.com', :active => 'true')
1045
+ example_10 = Zermelo::RedisExample.find_by_id('10')
1046
+
1047
+ time = Time.now.to_i
1048
+
1049
+ create_datum(example_8, :id => '3', :summary => 'aaargh', :timestamp => time.to_i + 20,
1050
+ :emotion => 'ok')
1051
+ create_datum(example_8, :id => '4', :summary => 'aaargh', :timestamp => time.to_i + 30,
1052
+ :emotion => 'ok')
1053
+ create_datum(example_10, :id => '5', :summary => 'aaargh', :timestamp => time.to_i + 40,
1054
+ :emotion => 'not_ok')
1055
+
1056
+ assoc_ids = Zermelo::RedisExample.intersect(:id => ['8', '9', '10']).
1057
+ associated_ids_for(:data)
1058
+ expect(assoc_ids).to eq('8' => Set.new(['3', '4']),
1059
+ '9' => Set.new(),
1060
+ '10' => Set.new(['5']))
1061
+ end
1062
+
1063
+ end
1064
+
1065
+ context "has_one" do
1066
+
1067
+ class Zermelo::RedisExampleSpecial
1068
+ include Zermelo::Records::RedisRecord
1069
+
1070
+ define_attributes :name => :string
1071
+
1072
+ belongs_to :example, :class_name => 'Zermelo::RedisExample', :inverse_of => :special
1073
+
1074
+ validates :name, :presence => true
1075
+ end
1076
+
1077
+ class Zermelo::RedisExample
1078
+ has_one :special, :class_name => 'Zermelo::RedisExampleSpecial', :inverse_of => :example
1079
+ end
1080
+
1081
+ it "sets and retrieves a record via a has_one association" do
1082
+ create_example(:id => '8', :name => 'John Jones',
1083
+ :email => 'jjones@example.com', :active => 'true')
1084
+
1085
+ special = Zermelo::RedisExampleSpecial.new(:id => '22', :name => 'Bill Smith')
1086
+ expect(special.save).to be_truthy
1087
+
1088
+ example = Zermelo::RedisExample.find_by_id('8')
1089
+ example.special = special
1090
+
1091
+ expect(redis.keys('*')).to match_array(['redis_example::attrs:ids',
1092
+ 'redis_example::indices:by_name',
1093
+ 'redis_example::indices:by_active:boolean:true',
1094
+ 'redis_example:8:attrs',
1095
+ 'redis_example:8:assocs',
1096
+ 'redis_example_special::attrs:ids',
1097
+ 'redis_example_special:22:attrs',
1098
+ 'redis_example_special:22:assocs:belongs_to'])
1099
+
1100
+ expect(redis.hgetall('redis_example:8:assocs')).to eq("special_id" => "22")
1101
+
1102
+ expect(redis.smembers('redis_example_special::attrs:ids')).to eq(['22'])
1103
+ expect(redis.hgetall('redis_example_special:22:attrs')).to eq(
1104
+ {'name' => 'Bill Smith'}
1105
+ )
1106
+
1107
+ expect(redis.hgetall('redis_example_special:22:assocs:belongs_to')).to eq(
1108
+ {'example_id' => '8'}
1109
+ )
1110
+
1111
+ example2 = Zermelo::RedisExample.find_by_id('8')
1112
+ special2 = example2.special
1113
+ expect(special2).not_to be_nil
1114
+
1115
+ expect(special2.id).to eq('22')
1116
+ expect(special2.example.id).to eq('8')
1117
+ end
1118
+
1119
+ def create_special(parent, attrs = {})
1120
+ redis.hmset("redis_example_special:#{attrs[:id]}:attrs", {'name' => attrs[:name]}.to_a.flatten)
1121
+
1122
+ redis.hset("redis_example_special:#{attrs[:id]}:assocs:belongs_to", 'example_id', parent.id)
1123
+ redis.hset("redis_example:#{parent.id}:assocs", 'special_id', attrs[:id])
1124
+
1125
+ redis.sadd('redis_example_special::attrs:ids', attrs[:id])
1126
+ end
1127
+
1128
+ it 'clears the belongs_to association when the child record is deleted' do
1129
+ create_example(:id => '8', :name => 'John Jones',
1130
+ :email => 'jjones@example.com', :active => 'true')
1131
+ example = Zermelo::RedisExample.find_by_id('8')
1132
+ create_special(example, :id => '3', :name => 'Another Jones')
1133
+ special = Zermelo::RedisExampleSpecial.find_by_id('3')
1134
+
1135
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
1136
+ 'redis_example::indices:by_name',
1137
+ 'redis_example::indices:by_active:boolean:true',
1138
+ 'redis_example:8:attrs',
1139
+ 'redis_example:8:assocs',
1140
+ 'redis_example_special::attrs:ids',
1141
+ 'redis_example_special:3:attrs',
1142
+ 'redis_example_special:3:assocs:belongs_to'])
1143
+
1144
+ special.destroy
1145
+
1146
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
1147
+ 'redis_example::indices:by_name',
1148
+ 'redis_example::indices:by_active:boolean:true',
1149
+ 'redis_example:8:attrs'])
1150
+ end
1151
+
1152
+ it "clears the belongs_to association when the parent record is deleted" do
1153
+ create_example(:id => '8', :name => 'John Jones',
1154
+ :email => 'jjones@example.com', :active => 'true')
1155
+ example = Zermelo::RedisExample.find_by_id('8')
1156
+ create_special(example, :id => '3', :name => 'Another Jones')
1157
+
1158
+ expect(redis.keys).to match_array(['redis_example::attrs:ids',
1159
+ 'redis_example::indices:by_name',
1160
+ 'redis_example::indices:by_active:boolean:true',
1161
+ 'redis_example:8:attrs',
1162
+ 'redis_example:8:assocs',
1163
+ 'redis_example_special::attrs:ids',
1164
+ 'redis_example_special:3:attrs',
1165
+ 'redis_example_special:3:assocs:belongs_to'])
1166
+
1167
+ example.destroy
1168
+
1169
+ expect(redis.keys).to match_array(['redis_example_special::attrs:ids',
1170
+ 'redis_example_special:3:attrs'])
1171
+ end
1172
+
1173
+ it 'returns associated ids for multiple parent ids' do
1174
+ create_example(:id => '8', :name => 'John Jones',
1175
+ :email => 'jjones@example.com', :active => 'true')
1176
+
1177
+ create_example(:id => '9', :name => 'Jane Johnson',
1178
+ :email => 'jjohnson@example.com', :active => 'true')
1179
+ example_9 = Zermelo::RedisExample.find_by_id('9')
1180
+
1181
+ create_example(:id => '10', :name => 'Jim Smith',
1182
+ :email => 'jsmith@example.com', :active => 'true')
1183
+ example_10 = Zermelo::RedisExample.find_by_id('10')
1184
+
1185
+ time = Time.now.to_i
1186
+
1187
+ create_special(example_9, :id => '3', :name => 'jkl')
1188
+ create_special(example_10, :id => '4', :name => 'pqr')
1189
+
1190
+ assoc_ids = Zermelo::RedisExample.intersect(:id => ['8', '9', '10']).
1191
+ associated_ids_for(:special)
1192
+ expect(assoc_ids).to eq('8' => nil,
1193
+ '9' => '3',
1194
+ '10' => '4')
1195
+ end
1196
+
1197
+ end
1198
+
1199
+
1200
+ context 'sorting by multiple keys' do
1201
+
1202
+ def create_template(attrs = {})
1203
+ redis.hmset("template:#{attrs[:id]}:attrs", {'name' => attrs[:name]}.to_a.flatten)
1204
+ redis.sadd('template::attrs:ids', attrs[:id])
1205
+ end
1206
+
1207
+ before do
1208
+ create_template(:id => '1', :name => 'abc')
1209
+ create_template(:id => '2', :name => 'def')
1210
+ create_template(:id => '3', :name => 'abc')
1211
+ create_template(:id => '4', :name => 'def')
1212
+ end
1213
+
1214
+ it 'sorts by multiple fields' do
1215
+ expect(Zermelo::Template.sort(:name => :asc, :id => :desc).map(&:id)).to eq(['3', '1', '4', '2'])
1216
+ end
1217
+
1218
+ end
1219
+
1220
+ context "has_and_belongs_to_many" do
1221
+
1222
+ def create_template(attrs = {})
1223
+ redis.hmset("template:#{attrs[:id]}:attrs", {'name' => attrs[:name]}.to_a.flatten)
1224
+ redis.sadd('template::attrs:ids', attrs[:id])
1225
+ end
1226
+
1227
+ before(:each) do
1228
+ create_example(:id => '8', :name => 'John Jones',
1229
+ :email => 'jjones@example.com', :active => true)
1230
+ create_template(:id => '2', :name => 'Template 1')
1231
+ end
1232
+
1233
+ it "sets a has_and_belongs_to_many relationship between two records in redis" do
1234
+ example = Zermelo::RedisExample.find_by_id('8')
1235
+ template = Zermelo::Template.find_by_id('2')
1236
+
1237
+ example.templates << template
1238
+
1239
+ expect(redis.keys('*')).to match_array(['redis_example::attrs:ids',
1240
+ 'redis_example::indices:by_name',
1241
+ 'redis_example::indices:by_active:boolean:true',
1242
+ 'redis_example:8:attrs',
1243
+ 'redis_example:8:assocs:templates_ids',
1244
+ 'template::attrs:ids',
1245
+ 'template:2:attrs',
1246
+ 'template:2:assocs:examples_ids'])
1247
+
1248
+ expect(redis.smembers('redis_example::attrs:ids')).to eq(['8'])
1249
+ expect(redis.smembers('redis_example::indices:by_active:boolean:true')).to eq(['8'])
1250
+ expect(redis.hgetall('redis_example:8:attrs')).to eq(
1251
+ {'name' => 'John Jones', 'email' => 'jjones@example.com', 'active' => 'true'}
1252
+ )
1253
+ expect(redis.smembers('redis_example:8:assocs:templates_ids')).to eq(['2'])
1254
+
1255
+ expect(redis.smembers('template::attrs:ids')).to eq(['2'])
1256
+ expect(redis.hgetall('template:2:attrs')).to eq({'name' => 'Template 1'})
1257
+ expect(redis.smembers('template:2:assocs:examples_ids')).to eq(['8'])
1258
+ end
1259
+
1260
+ it "loads a record from a has_and_belongs_to_many relationship" do
1261
+ example = Zermelo::RedisExample.find_by_id('8')
1262
+ template = Zermelo::Template.find_by_id('2')
1263
+
1264
+ template.examples << example
1265
+
1266
+ templates = example.templates.all
1267
+
1268
+ expect(templates).to be_an(Array)
1269
+ expect(templates.size).to eq(1)
1270
+ other_template = templates.first
1271
+ expect(other_template).to be_a(Zermelo::Template)
1272
+ expect(other_template.id).to eq(template.id)
1273
+ end
1274
+
1275
+ it "removes a has_and_belongs_to_many relationship between two records in redis" do
1276
+ example = Zermelo::RedisExample.find_by_id('8')
1277
+ template = Zermelo::Template.find_by_id('2')
1278
+
1279
+ template.examples << example
1280
+
1281
+ expect(redis.smembers('template::attrs:ids')).to eq(['2'])
1282
+ expect(redis.smembers('redis_example:8:assocs:templates_ids')).to eq(['2'])
1283
+
1284
+ example.templates.delete(template)
1285
+
1286
+ expect(redis.smembers('template::attrs:ids')).to eq(['2']) # template not deleted
1287
+ expect(redis.smembers('redis_example:8:assocs:templates_ids')).to eq([]) # but association is
1288
+ end
1289
+
1290
+ it "filters has_and_belongs_to_many records by indexed attribute values" do
1291
+ create_example(:id => '9', :name => 'James Smith',
1292
+ :email => 'jsmith@example.com', :active => false)
1293
+ create_example(:id => '10', :name => 'Alpha Beta',
1294
+ :email => 'abc@example.com', :active => true)
1295
+
1296
+ example = Zermelo::RedisExample.find_by_id('8')
1297
+ example_2 = Zermelo::RedisExample.find_by_id('9')
1298
+ example_3 = Zermelo::RedisExample.find_by_id('10')
1299
+ template = Zermelo::Template.find_by_id('2')
1300
+
1301
+ example.templates << template
1302
+ example_2.templates << template
1303
+ example_3.templates << template
1304
+
1305
+ examples = template.examples.intersect(:active => true).all
1306
+ expect(examples).not_to be_nil
1307
+ expect(examples).to be_an(Array)
1308
+ expect(examples.size).to eq(2)
1309
+ expect(examples.map(&:id)).to match_array(['8', '10'])
1310
+ end
1311
+
1312
+ it "checks whether a record id exists through a has_and_belongs_to_many filter" do
1313
+ create_example(:id => '9', :name => 'James Smith',
1314
+ :email => 'jsmith@example.com', :active => false)
1315
+
1316
+ example = Zermelo::RedisExample.find_by_id('8')
1317
+ example_2 = Zermelo::RedisExample.find_by_id('9')
1318
+ template = Zermelo::Template.find_by_id('2')
1319
+
1320
+ example.templates << template
1321
+ example_2.templates << template
1322
+
1323
+ expect(template.examples.intersect(:active => false).exists?('9')).to be_truthy
1324
+ expect(template.examples.intersect(:active => false).exists?('8')).to be_falsey
1325
+ end
1326
+
1327
+ it "finds a record through a has_and_belongs_to_many filter" do
1328
+ create_example(:id => '9', :name => 'James Smith',
1329
+ :email => 'jsmith@example.com', :active => false)
1330
+
1331
+ example = Zermelo::RedisExample.find_by_id('8')
1332
+ example_2 = Zermelo::RedisExample.find_by_id('9')
1333
+ template = Zermelo::Template.find_by_id('2')
1334
+
1335
+ example.templates << template
1336
+ example_2.templates << template
1337
+
1338
+ james = template.examples.intersect(:active => false).find_by_id('9')
1339
+ expect(james).not_to be_nil
1340
+ expect(james).to be_a(Zermelo::RedisExample)
1341
+ expect(james.id).to eq(example_2.id)
1342
+ end
1343
+
1344
+ it 'clears a has_and_belongs_to_many association when a record is deleted'
1345
+
1346
+ it 'returns associated ids for multiple parent ids' do
1347
+ create_example(:id => '9', :name => 'Jane Johnson',
1348
+ :email => 'jjohnson@example.com', :active => 'true')
1349
+ example_9 = Zermelo::RedisExample.find_by_id('9')
1350
+
1351
+ create_example(:id => '10', :name => 'Jim Smith',
1352
+ :email => 'jsmith@example.com', :active => 'true')
1353
+ example_10 = Zermelo::RedisExample.find_by_id('10')
1354
+
1355
+ create_template(:id => '3', :name => 'Template 3')
1356
+ create_template(:id => '4', :name => 'Template 4')
1357
+
1358
+ template_2 = Zermelo::Template.find_by_id('2')
1359
+ template_3 = Zermelo::Template.find_by_id('3')
1360
+ template_4 = Zermelo::Template.find_by_id('4')
1361
+
1362
+ example_9.templates.add(template_2)
1363
+ example_10.templates.add(template_3, template_4)
1364
+
1365
+ assoc_ids = Zermelo::RedisExample.intersect(:id => ['8', '9', '10']).
1366
+ associated_ids_for(:templates)
1367
+ expect(assoc_ids).to eq('8' => Set.new([]),
1368
+ '9' => Set.new(['2']),
1369
+ '10' => Set.new(['3', '4']))
1370
+ end
1371
+
1372
+ end
1373
+
1374
+ context 'bad parameters' do
1375
+
1376
+ let(:example) { Zermelo::RedisExample.find_by_id('8') }
1377
+
1378
+ before(:each) do
1379
+ create_example(:id => '8', :name => 'John Jones',
1380
+ :email => 'jjones@example.com', :active => true)
1381
+ end
1382
+
1383
+ it 'raises an error when calling add on has_many without an argument' do
1384
+ expect {
1385
+ example.children.add
1386
+ }.to raise_error
1387
+ end
1388
+
1389
+ it 'raises an error when calling delete on has_many without an argument' do
1390
+ expect {
1391
+ example.children.delete
1392
+ }.to raise_error
1393
+ end
1394
+
1395
+ it 'raises an error when calling add on has_sorted_set without an argument' do
1396
+ expect {
1397
+ example.data.add
1398
+ }.to raise_error
1399
+ end
1400
+
1401
+ it 'raises an error when calling delete on has_sorted_set without an argument' do
1402
+ expect {
1403
+ example.data.delete
1404
+ }.to raise_error
1405
+ end
1406
+
1407
+ it 'raises an error when calling add on has_and_belongs_to_many without an argument' do
1408
+ expect {
1409
+ example.templates.add
1410
+ }.to raise_error
1411
+ end
1412
+
1413
+ it 'raises an error when calling delete on has_and_belongs_to_many without an argument' do
1414
+ expect {
1415
+ example.templates.delete
1416
+ }.to raise_error
1417
+ end
1418
+
1419
+ it 'raises an error when trying to filter on a non-indexed value' do
1420
+ expect {
1421
+ Zermelo::RedisExample.intersect(:email => 'jjones@example.com').all
1422
+ }.to raise_error
1423
+ end
1424
+ end
1425
+
1426
+ end