zermelo 1.0.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.
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