zermelo 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/.travis.yml +3 -5
  4. data/CHANGELOG.md +8 -2
  5. data/lib/zermelo/associations/belongs_to.rb +3 -3
  6. data/lib/zermelo/associations/has_and_belongs_to_many.rb +3 -3
  7. data/lib/zermelo/associations/has_many.rb +3 -3
  8. data/lib/zermelo/associations/has_one.rb +3 -3
  9. data/lib/zermelo/associations/has_sorted_set.rb +4 -4
  10. data/lib/zermelo/associations/index.rb +1 -2
  11. data/lib/zermelo/associations/unique_index.rb +1 -2
  12. data/lib/zermelo/backends/base.rb +1 -1
  13. data/lib/zermelo/backends/influxdb_backend.rb +29 -18
  14. data/lib/zermelo/backends/redis_backend.rb +106 -6
  15. data/lib/zermelo/filters/base.rb +34 -57
  16. data/lib/zermelo/filters/influxdb_filter.rb +22 -70
  17. data/lib/zermelo/filters/redis_filter.rb +35 -482
  18. data/lib/zermelo/filters/steps/list_step.rb +79 -0
  19. data/lib/zermelo/filters/steps/set_step.rb +176 -0
  20. data/lib/zermelo/filters/steps/sort_step.rb +85 -0
  21. data/lib/zermelo/filters/steps/sorted_set_step.rb +156 -0
  22. data/lib/zermelo/records/class_methods.rb +16 -4
  23. data/lib/zermelo/records/influxdb_record.rb +2 -0
  24. data/lib/zermelo/records/instance_methods.rb +4 -4
  25. data/lib/zermelo/records/key.rb +2 -0
  26. data/lib/zermelo/version.rb +1 -1
  27. data/lib/zermelo.rb +9 -1
  28. data/spec/lib/zermelo/records/influxdb_record_spec.rb +186 -10
  29. data/spec/lib/zermelo/records/redis_record_spec.rb +11 -4
  30. data/spec/spec_helper.rb +12 -10
  31. metadata +5 -11
  32. data/lib/zermelo/filters/steps/diff_range_step.rb +0 -17
  33. data/lib/zermelo/filters/steps/diff_step.rb +0 -17
  34. data/lib/zermelo/filters/steps/intersect_range_step.rb +0 -17
  35. data/lib/zermelo/filters/steps/intersect_step.rb +0 -17
  36. data/lib/zermelo/filters/steps/limit_step.rb +0 -17
  37. data/lib/zermelo/filters/steps/offset_step.rb +0 -17
  38. data/lib/zermelo/filters/steps/union_range_step.rb +0 -17
  39. data/lib/zermelo/filters/steps/union_step.rb +0 -17
  40. data/lib/zermelo/records/collection.rb +0 -14
@@ -8,46 +8,21 @@ module Zermelo
8
8
 
9
9
  class RedisFilter
10
10
 
11
- # abstraction for a set or list of record ids
12
- class Collection
13
- attr_reader :name, :type
14
- def initialize(opts = {})
15
- @name = opts[:name]
16
- @type = opts[:type]
17
- end
18
- end
19
-
20
11
  include Zermelo::Filters::Base
21
12
 
13
+ # TODO polite error when first/last applied to set
14
+
22
15
  # more step users
23
16
  def first
24
- unless [:list, :sorted_set].include?(@initial_set.type) ||
25
- @steps.any? {|s| s.is_a?(Zermelo::Filters::Steps::SortStep) }
26
-
27
- raise "Can't get first member of a non-sorted set"
28
- end
29
-
30
17
  lock {
31
- first_id = resolve_steps do |collection|
32
- op = {:list => :lrange, :sorted_set => :zrange}[collection.type]
33
- Zermelo.redis.send(op, collection.name, 0, 0).first
34
- end
18
+ first_id = resolve_steps(:first)
35
19
  first_id.nil? ? nil : _load(first_id)
36
20
  }
37
21
  end
38
22
 
39
23
  def last
40
- unless [:list, :sorted_set].include?(@initial_set.type) ||
41
- @steps.any? {|s| s.is_a?(Zermelo::Filters::Steps::SortStep) }
42
-
43
- raise "Can't get last member of a non-sorted set"
44
- end
45
-
46
24
  lock {
47
- last_id = resolve_steps do |collection|
48
- op = {:list => :lrevrange, :sorted_set => :zrevrange}[collection.type]
49
- Zermelo.redis.send(op, collection.name, 0, 0).first
50
- end
25
+ last_id = resolve_steps(:last)
51
26
  last_id.nil? ? nil : _load(last_id)
52
27
  }
53
28
  end
@@ -56,214 +31,18 @@ module Zermelo
56
31
  private
57
32
 
58
33
  def _count
59
- resolve_steps(:list => :llen,
60
- :set => :scard,
61
- :sorted_set => :zcard)
34
+ resolve_steps(:count)
62
35
  end
63
36
 
64
37
  def _ids
65
- resolve_steps(:list => :lrange,
66
- :set => :smembers,
67
- :sorted_set => :zrange)
38
+ resolve_steps(:ids)
68
39
  end
69
40
 
70
41
  def _exists?(id)
71
42
  return if id.nil?
72
- resolve_steps do |collection|
73
- case collection.type
74
- when :list
75
- Zermelo.redis.lrange(collection.name, 0, -1).include?(id)
76
- when :set
77
- Zermelo.redis.sismember(collection.name, id)
78
- when :sorted_set
79
- !Zermelo.redis.zscore(collection.name, id).nil?
80
- end
81
- end
82
- end
83
-
84
- def temp_set_name
85
- "#{class_key}::tmp:#{SecureRandom.hex(16)}"
86
- end
87
-
88
- def class_key
89
- @class_key ||= @associated_class.send(:class_key)
90
- end
91
-
92
- def indexed_step_to_set(att, idx_class, value, attr_type)
93
- # TODO (maybe) if a filter from different backend, resolve to ids and
94
- # put that in a Redis temp set
95
-
96
- case value
97
- when Zermelo::Filters::RedisFilter
98
-
99
- collection, should_be_deleted = value.resolve_steps
100
-
101
- if should_be_deleted
102
- temp_sets << collection.name
103
- end
104
-
105
- unless :set.eql?(collection.type)
106
- raise "Unsure as yet if non-sets are safe as Filter step values"
107
- end
108
-
109
- when Regexp
110
- raise "Can't query non-string values via regexp" unless :string.eql?(attr_type)
111
- idx_result = temp_set_name
112
- starts_with_string_re = /^string:/
113
- case idx_class.name
114
- when 'Zermelo::Associations::UniqueIndex'
115
- index_key = backend.key_to_redis_key(Zermelo::Records::Key.new(
116
- :klass => class_key,
117
- :name => "by_#{att}",
118
- :type => :hash,
119
- :object => :index
120
- ))
121
- candidates = Zermelo.redis.hgetall(index_key)
122
- matching_ids = candidates.values_at(*candidates.keys.select {|k|
123
- (starts_with_string_re === k) &&
124
- (value === backend.unescape_key_name(k.sub(starts_with_string_re, '')))
125
- })
126
- Zermelo.redis.sadd(idx_result, matching_ids) unless matching_ids.empty?
127
- when 'Zermelo::Associations::Index'
128
- key_root = backend.key_to_redis_key(Zermelo::Records::Key.new(
129
- :klass => class_key,
130
- :name => "by_#{att}:string",
131
- :type => :set,
132
- :object => :index
133
- ))
134
-
135
- matching_sets = Zermelo.redis.keys(key_root + ":*").inject([]) do |memo, k|
136
- k =~ /^#{key_root}:(.+)$/
137
- memo << k if value === $1
138
- memo
139
- end
140
-
141
- Zermelo.redis.sunionstore(idx_result, matching_sets) unless matching_sets.empty?
142
- end
143
- [idx_result, true]
144
- else
145
- index = @associated_class.send("#{att}_index")
146
-
147
- case index
148
- when Zermelo::Associations::UniqueIndex
149
- idx_result = temp_set_name
150
- Zermelo.redis.sadd(idx_result,
151
- Zermelo.redis.hget(backend.key_to_redis_key(index.key),
152
- backend.index_keys(attr_type, value).join(':')))
153
- [idx_result, true]
154
- when Zermelo::Associations::Index
155
- [backend.key_to_redis_key(index.key(value)), false]
156
- end
157
- end
158
- end
159
-
160
- def resolve_step(step, source, idx_attrs, attr_types, &block)
161
- temp_sets = []
162
- source_keys = []
163
-
164
- case step
165
- when Zermelo::Filters::Steps::IntersectStep,
166
- Zermelo::Filters::Steps::UnionStep,
167
- Zermelo::Filters::Steps::DiffStep
168
-
169
- source_keys += step.attributes.inject([]) do |memo, (att, value)|
170
-
171
- val = value.is_a?(Set) ? value.to_a : value
172
-
173
- if :id.eql?(att)
174
- ts = temp_set_name
175
- temp_sets << ts
176
- Zermelo.redis.sadd(ts, val)
177
- memo << ts
178
- else
179
- idx_class = idx_attrs[att.to_s]
180
- raise "'#{att}' property is not indexed" if idx_class.nil?
181
-
182
- if val.is_a?(Enumerable)
183
- conditions_set = temp_set_name
184
- temp_idx_sets = []
185
- Zermelo.redis.sunionstore(conditions_set, *val.collect {|v|
186
- idx_set, clear = indexed_step_to_set(att, idx_class, v, attr_types[att])
187
- temp_idx_sets << idx_set if clear
188
- idx_set
189
- })
190
- Zermelo.redis.del(temp_idx_sets) unless temp_idx_sets.empty?
191
- temp_sets << conditions_set
192
- memo << conditions_set
193
- else
194
- idx_set, clear = indexed_step_to_set(att, idx_class, val, attr_types[att])
195
- temp_sets << idx_set if clear
196
- memo << idx_set
197
- end
198
- end
199
-
200
- memo
201
- end
202
-
203
- when Zermelo::Filters::Steps::IntersectRangeStep,
204
- Zermelo::Filters::Steps::UnionRangeStep,
205
- Zermelo::Filters::Steps::DiffRangeStep
206
-
207
- range_ids_set = temp_set_name
208
-
209
- options = step.options || {}
210
-
211
- start = options[:start]
212
- finish = options[:finish]
213
-
214
- order_desc = options[:desc].is_a?(TrueClass)
215
-
216
- if options[:by_score]
217
- start = '-inf' if start.nil? || (start <= 0)
218
- finish = '+inf' if finish.nil? || (finish <= 0)
219
- else
220
- start = 0 if start.nil?
221
- finish = -1 if finish.nil?
222
- end
223
-
224
- args = [start, finish]
225
-
226
- if order_desc
227
- if options[:by_score]
228
- query = :zrevrangebyscore
229
- args = args.map(&:to_s).reverse
230
- else
231
- query = :zrevrange
232
- end
233
- elsif options[:by_score]
234
- query = :zrangebyscore
235
- args = args.map(&:to_s)
236
- else
237
- query = :zrange
238
- end
239
-
240
- args << {:with_scores => :true}
241
-
242
- if options[:limit]
243
- args.last.update(:limit => [0, options[:limit].to_i])
244
- end
245
-
246
- args.unshift(source)
247
-
248
- range_ids_scores = Zermelo.redis.send(query, *args)
249
-
250
- unless range_ids_scores.empty?
251
- Zermelo.redis.zadd(range_ids_set, range_ids_scores.map(&:reverse))
252
- end
253
- source_keys << range_ids_set
254
- temp_sets << range_ids_set
255
- end
256
-
257
- yield(source_keys)
258
-
259
- unless temp_sets.empty?
260
- Zermelo.redis.del(*temp_sets)
261
- temp_sets.clear
262
- end
43
+ resolve_steps(:exists?, id)
263
44
  end
264
45
 
265
- # TODO could parts of this move to a stored Lua script in the redis server?
266
-
267
46
  # If called with a block -- takes a block and passes the name of a set to
268
47
  # it; deletes all temporary sets once done
269
48
 
@@ -273,35 +52,22 @@ module Zermelo
273
52
  # the name of a set containing the filtered ids, the second is a boolean
274
53
  # for whether or not to clear up that set once it's been used
275
54
 
276
- def resolve_steps(shortcuts = {}, &block)
277
- source = backend.key_to_redis_key(@initial_set)
278
- source_type = @initial_set.type
279
-
55
+ def resolve_steps(shortcut, *args)
280
56
  if @steps.empty?
281
- ret = if shortcuts.empty?
282
- data = Zermelo::Filters::RedisFilter::Collection.new(
283
- :name => source, :type => source_type)
284
- if block.nil?
285
- [data, false]
286
- else
287
- block.call(data)
288
- end
57
+ sc_klass = {
58
+ :list => Zermelo::Filters::Steps::ListStep,
59
+ :set => Zermelo::Filters::Steps::SetStep,
60
+ :sorted_set => Zermelo::Filters::Steps::SortedSetStep
61
+ }[@initial_key.type]
62
+ sc = sc_klass.const_get(:REDIS_SHORTCUTS)[shortcut]
63
+ ret = if sc.nil?
64
+ yield(@initial_key)
289
65
  else
290
- if :sorted_set.eql?(source_type) && :zrange.eql?(shortcuts[source_type])
291
- Zermelo.redis.zrange(source, 0, -1)
292
- elsif :list.eql?(source_type) && :lrange.eql?(shortcuts[source_type])
293
- Zermelo.redis.lrange(source, 0, -1)
294
- else
295
- Zermelo.redis.send(shortcuts[source_type], source)
296
- end
66
+ sc.call(*([backend.key_to_redis_key(@initial_key)] + args))
297
67
  end
298
- return ret
68
+ return(ret)
299
69
  end
300
70
 
301
- temp_sets = []
302
- dest_set = nil
303
- ret = nil
304
-
305
71
  idx_attrs = @associated_class.send(:with_index_data) do |d|
306
72
  d.each_with_object({}) do |(name, data), memo|
307
73
  memo[name.to_s] = data.index_klass
@@ -310,246 +76,33 @@ module Zermelo
310
76
 
311
77
  attr_types = @associated_class.send(:attribute_types)
312
78
 
313
- offset = nil
314
- limit = nil
315
- order_desc = nil
316
-
317
- members = nil
318
-
319
- begin
320
-
321
- @steps.each_with_index do |step, idx|
322
-
323
- resolve_step(step, source, idx_attrs, attr_types) do |source_keys|
324
- options = step.options || {}
325
-
326
- order_desc = order_desc ^ options[:desc].is_a?(TrueClass)
327
-
328
- unless step.class.accepted_types.include?(source_type)
329
- raise "'#{step.class.name}' does not accept input type #{source_type}"
330
- end
331
-
332
- case source_type
333
- when :set
334
- sort_set = if step.is_a?(Zermelo::Filters::Steps::SortStep)
335
-
336
- proc {
337
-
338
- # TODO raise error in step construction if keys not
339
- # passed as expected below
340
- sort_attrs_and_orders = case options[:keys]
341
- when String, Symbol
342
- {options[:keys].to_s => options[:desc].is_a?(TrueClass) ? :desc : :asc}
343
- when Array
344
- options[:keys].each_with_object({}) do |k, memo|
345
- memo[k.to_sym] = (options[:desc].is_a?(TrueClass) ? :desc : :asc)
346
- end
347
- when Hash
348
- options[:keys]
349
- end
350
-
351
- # TODO check if complex attribute types or associations
352
- # can be used for sorting
353
-
354
- Zermelo.redis.sunionstore(dest_set, source, *source_keys)
355
-
356
- sort_attrs_and_orders.keys.reverse.each_with_index do |sort_attr, idx|
357
-
358
- order = sort_attrs_and_orders[sort_attr]
359
-
360
- opts = {}
79
+ backend.temp_key_wrap do |temp_keys|
80
+ result = nil
81
+ last_step = @steps.last
361
82
 
362
- unless 'id'.eql?(sort_attr.to_s)
363
- opts.update(:by => "#{class_key}:*:attrs->#{sort_attr}")
364
- end
83
+ step_opts = {
84
+ :index_attrs => idx_attrs,
85
+ :attr_types => attr_types,
86
+ :temp_keys => temp_keys,
87
+ :source => @initial_key
88
+ }
365
89
 
366
- if (idx + 1) == sort_attrs_and_orders.size
367
- # only apply offset & limit on the last sort
368
- o = options[:offset]
369
- l = options[:limit]
370
-
371
- if !(l.nil? && o.nil?)
372
- o = o.nil? ? 0 : o.to_i
373
- l = (l.nil? || (l.to_i < 1)) ? (Zermelo.redis.llen(dest_set) - o) : l
374
- opts.update(:limit => [o, l])
375
- end
376
- end
377
-
378
- order_parts = []
379
- sort_attr_type = attr_types[sort_attr.to_sym]
380
- unless [:integer, :float, :timestamp].include?(sort_attr_type)
381
- order_parts << 'alpha'
382
- end
383
- order_parts << 'desc' if 'desc'.eql?(order.to_s)
384
-
385
- unless order_parts.empty?
386
- opts.update(:order => order_parts.join(' '))
387
- end
388
-
389
- opts.update(:store => dest_set)
390
- Zermelo.redis.sort(dest_set, opts)
391
- end
392
-
393
- source_type = :list
394
- }
395
- else
396
- nil
397
- end
398
-
399
- last_step_and_smembers = (idx == (@steps.size - 1)) && :smembers.eql?(shortcuts[:set])
400
-
401
- unless last_step_and_smembers && step.is_a?(Zermelo::Filters::Steps::IntersectStep)
402
- dest_set = temp_set_name
403
- temp_sets << dest_set
404
- end
405
-
406
- if last_step_and_smembers
407
- members = case step
408
- when Zermelo::Filters::Steps::UnionStep
409
- Zermelo.redis.sinterstore(dest_set, *source_keys)
410
- Zermelo.redis.sunion(dest_set, source)
411
- when Zermelo::Filters::Steps::IntersectStep
412
- Zermelo.redis.sinter(source, *source_keys)
413
- when Zermelo::Filters::Steps::DiffStep
414
- Zermelo.redis.sinterstore(dest_set, *source_keys)
415
- Zermelo.redis.sdiff(source, dest_set)
416
- when Zermelo::Filters::Steps::SortStep
417
- sort_set.call
418
- Zermelo.redis.send((order_desc ? :lrevrange : :lrange),
419
- dest_set, 0, -1)
420
- end
421
- else
422
-
423
- case step
424
- when Zermelo::Filters::Steps::UnionStep
425
- Zermelo.redis.sinterstore(dest_set, *source_keys)
426
- Zermelo.redis.sunionstore(dest_set, source, dest_set)
427
- when Zermelo::Filters::Steps::IntersectStep
428
- Zermelo.redis.sinterstore(dest_set, *source_keys)
429
- when Zermelo::Filters::Steps::DiffStep
430
- Zermelo.redis.sinterstore(dest_set, *source_keys)
431
- Zermelo.redis.sdiffstore(dest_set, source, dest_set)
432
- when Zermelo::Filters::Steps::SortStep
433
- sort_set.call
434
- end
435
-
436
- source = dest_set
437
- end
438
-
439
- when :list
440
- # TODO could allow reversion into set by :union, :intersect, :diff,
441
- # or application of :sort again to re-order. For now, YAGNI, and
442
- # document the limitations.
443
-
444
- case step
445
- when Zermelo::Filters::Steps::OffsetStep
446
- offset = options[:amount]
447
- when Zermelo::Filters::Steps::LimitStep
448
- limit = options[:amount]
449
- end
450
-
451
- when :sorted_set
452
- weights = case step
453
- when Zermelo::Filters::Steps::UnionStep, Zermelo::Filters::Steps::UnionRangeStep
454
- [0.0] * source_keys.length
455
- when Zermelo::Filters::Steps::DiffStep, Zermelo::Filters::Steps::DiffRangeStep
456
- [-1.0] * source_keys.length
457
- end
458
-
459
- dest_set = temp_set_name
460
- temp_sets << dest_set
461
-
462
- case step
463
- when Zermelo::Filters::Steps::UnionStep, Zermelo::Filters::Steps::UnionRangeStep
464
- Zermelo.redis.zinterstore(dest_set, source_keys, :weights => weights, :aggregate => 'max')
465
- Zermelo.redis.zunionstore(dest_set, [source, dest_set])
466
- when Zermelo::Filters::Steps::IntersectStep, Zermelo::Filters::Steps::IntersectRangeStep
467
- Zermelo.redis.zinterstore(dest_set, [source] + source_keys, :weights => weights, :aggregate => 'max')
468
- when Zermelo::Filters::Steps::DiffStep, Zermelo::Filters::Steps::DiffRangeStep
469
- # 'zdiffstore' via weights, relies on non-zero scores being used
470
- # see https://code.google.com/p/redis/issues/detail?id=579
471
- Zermelo.redis.zinterstore(dest_set, source_keys, :weights => weights, :aggregate => 'max')
472
- Zermelo.redis.zunionstore(dest_set, [source, dest_set])
473
- Zermelo.redis.zremrangebyscore(dest_set, "0", "0")
474
- end
475
-
476
- if (idx == (@steps.size - 1)) && :zrange.eql?(shortcuts[:sorted_set])
477
- # supporting shortcut structures here as it helps preserve the
478
- # win gained by the shortcut for empty steps, but this is
479
- # no better than passing it through to a block would be; if
480
- # Redis still supported ZINTER and ZUNION it would work better
481
- members = Zermelo.redis.send((order_desc ? :zrevrange : :zrange),
482
- dest_set, 0, -1)
483
- else
484
- source = dest_set
485
- end
486
-
487
- end
90
+ @steps.each do |step|
91
+ unless step.class.accepted_types.include?(step_opts[:source].type)
92
+ raise "'#{step.class.name}' does not accept input type #{step_opts[:source].type}"
488
93
  end
489
94
 
490
- end
491
-
492
- ret = if members.nil?
493
- if :list.eql?(source_type) && !(offset.nil? && limit.nil?)
494
-
495
- # TODO need a guaranteed non-existing key for non-sorting 'sort'
496
- o = offset.to_i
497
- l = limit.to_i
498
- l = (Zermelo.redis.llen(dest_set) - o) if (limit < 1)
499
-
500
- opts = {:by => 'no_sort', :limit => [o, l]}
501
-
502
- # https://github.com/antirez/redis/issues/2079, fixed in redis 2.8.19
503
- if (Zermelo.redis_version <=> ['2', '8', '18']) == 1
504
- opts.update(:store => dest_set)
505
- Zermelo.redis.sort(dest_set, opts)
506
- else
507
- data = Zermelo.redis.sort(dest_set, opts)
508
-
509
- if data.empty?
510
- Zermelo.redis.del(dest_set)
511
- else
512
- limited = temp_set_name
513
- temp_sets << limited
514
-
515
- Zermelo.redis.rpush(limited, data)
516
-
517
- dest_set = limited
518
- end
519
- end
95
+ if step == last_step
96
+ step_opts.update(:shortcut => shortcut, :shortcut_args => args)
520
97
  end
521
98
 
522
- if shortcuts.empty?
523
- data = Zermelo::Filters::RedisFilter::Collection.new(
524
- :name => dest_set, :type => source_type)
525
- if block.nil?
526
- should_be_deleted = !temp_sets.delete(dest_set).nil?
527
- [data, should_be_deleted]
528
- else
529
- block.call(data)
530
- end
531
- elsif :sorted_set.eql?(source_type) && :zrange.eql?(shortcuts[source_type])
532
- Zermelo.redis.zrange(dest_set, 0, -1)
533
- elsif :list.eql?(source_type) && :lrange.eql?(shortcuts[source_type])
534
- Zermelo.redis.lrange(dest_set, 0, -1)
535
- else
536
- Zermelo.redis.send(shortcuts[source_type], dest_set)
537
- end
99
+ result = step.resolve(backend, @associated_class, step_opts)
538
100
 
539
- else
540
- members
101
+ step_opts[:source] = result unless step == last_step
541
102
  end
542
103
 
543
- rescue
544
- raise
545
- ensure
546
- unless temp_sets.empty?
547
- Zermelo.redis.del(*temp_sets)
548
- temp_sets.clear
549
- end
104
+ result
550
105
  end
551
-
552
- ret
553
106
  end
554
107
  end
555
108
 
@@ -0,0 +1,79 @@
1
+ require 'zermelo/filters/steps/base_step'
2
+
3
+ module Zermelo
4
+ module Filters
5
+ class Steps
6
+ class ListStep < Zermelo::Filters::Steps::BaseStep
7
+ def self.accepted_types
8
+ [:list]
9
+ end
10
+
11
+ def self.returns_type
12
+ :list
13
+ end
14
+
15
+ REDIS_SHORTCUTS = {
16
+ :ids => proc {|key| Zermelo.redis.lrange(key, 0, -1) },
17
+ :count => proc {|key| Zermelo.redis.llen(key) },
18
+ :exists? => proc {|key, id| Zermelo.redis.lrange(key, 0, -1).include?(id) },
19
+ :first => proc {|key| Zermelo.redis.lrange(key, 0, 0).first },
20
+ :last => proc {|key| Zermelo.redis.lrevrange(key, 0, 0).first }
21
+ }
22
+
23
+ def resolve(backend, associated_class, opts = {})
24
+ shortcut = opts[:shortcut]
25
+
26
+ offset = @options[:offset]
27
+ limit = @options[:limit]
28
+
29
+ o = offset.to_i
30
+ l = limit.to_i
31
+
32
+ case backend
33
+ when Zermelo::Backends::RedisBackend
34
+
35
+ source = opts[:source]
36
+ idx_attrs = opts[:index_attrs]
37
+ attr_types = opts[:attr_types]
38
+ temp_keys = opts[:temp_keys]
39
+
40
+ # TODO apply these transformations via a subset?
41
+ # TODO need a guaranteed non-existing key for non-sorting 'sort'
42
+
43
+ # TODO check if source is in temp_keys, use a generated temp_key instead if not
44
+ r_source = backend.key_to_redis_key(source)
45
+
46
+ l = (Zermelo.redis.llen(r_source) - o) if (l < 1)
47
+
48
+ sort_opts = {:by => 'no_sort', :limit => [o, l]}
49
+
50
+ # https://github.com/antirez/redis/issues/2079, fixed in redis 2.8.19
51
+ result, r_result = if (Zermelo.redis_version.split('.') <=> ['2', '8', '18']) == 1
52
+ sort_opts.update(:store => r_source)
53
+ Zermelo.redis.sort(r_source, sort_opts)
54
+ [source, r_source]
55
+ else
56
+ data = Zermelo.redis.sort(r_source, sort_opts)
57
+
58
+ if data.empty?
59
+ # TODO fix
60
+ else
61
+ limited = associated_class.send(:temp_key, :list)
62
+ temp_keys << limited
63
+ r_limited = backend.key_to_redis_key(limited)
64
+
65
+ Zermelo.redis.rpush(r_limited, data)
66
+
67
+ [limited, r_limited]
68
+ end
69
+ end
70
+
71
+ return result if shortcut.nil?
72
+ REDIS_SHORTCUTS[shortcut].call(*([r_result] + opts[:shortcut_args]))
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end