zermelo 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,176 @@
1
+ require 'zermelo/filters/steps/base_step'
2
+
3
+ # NB: temp keys for now are bare redis keys, should be full Key objects
4
+ module Zermelo
5
+ module Filters
6
+ class Steps
7
+ class SetStep < Zermelo::Filters::Steps::BaseStep
8
+ def self.accepted_types
9
+ [:set, :sorted_set] # TODO should allow :list as well
10
+ end
11
+
12
+ def self.returns_type
13
+ :set
14
+ end
15
+
16
+ REDIS_SHORTCUTS = {
17
+ :ids => proc {|key| Zermelo.redis.smembers(key) },
18
+ :count => proc {|key| Zermelo.redis.scard(key) },
19
+ :exists? => proc {|key, id| Zermelo.redis.sismember(key, id) }
20
+ }
21
+
22
+ def resolve(backend, associated_class, opts = {})
23
+
24
+ case backend
25
+ when Zermelo::Backends::RedisBackend
26
+ source = opts[:source]
27
+ idx_attrs = opts[:index_attrs]
28
+ attr_types = opts[:attr_types]
29
+ temp_keys = opts[:temp_keys]
30
+
31
+ source_keys = @attributes.inject([]) do |memo, (att, value)|
32
+
33
+ val = value.is_a?(Set) ? value.to_a : value
34
+
35
+ if :id.eql?(att)
36
+ ts = associated_class.send(:temp_key, :set)
37
+ temp_keys << ts
38
+ Zermelo.redis.sadd(backend.key_to_redis_key(ts), val)
39
+ memo << ts
40
+ else
41
+ idx_class = idx_attrs[att.to_s]
42
+ raise "'#{att}' property is not indexed" if idx_class.nil?
43
+
44
+ if val.is_a?(Enumerable)
45
+ conditions_set = associated_class.send(:temp_key, :set)
46
+ r_conditions_set = backend.key_to_redis_key(conditions_set)
47
+
48
+ backend.temp_key_wrap do |conditions_temp_keys|
49
+ index_keys = val.collect {|v|
50
+ il = backend.index_lookup(att, associated_class,
51
+ idx_class, v, attr_types[att], conditions_temp_keys)
52
+ backend.key_to_redis_key(il)
53
+ }
54
+
55
+ Zermelo.redis.sunionstore(r_conditions_set, *index_keys)
56
+ end
57
+ memo << conditions_set
58
+ else
59
+ memo << backend.index_lookup(att, associated_class,
60
+ idx_class, val, attr_types[att], temp_keys)
61
+ end
62
+ end
63
+
64
+ memo
65
+ end
66
+
67
+ case source.type
68
+ when :sorted_set
69
+ Zermelo::Filters::Steps::SortedSetStep.evaluate(backend,
70
+ @options[:op], associated_class, source, source_keys, temp_keys, opts)
71
+ when :set
72
+ self.class.evaluate(backend, @options[:op], associated_class,
73
+ source, source_keys, temp_keys, opts)
74
+ end
75
+
76
+ when Zermelo::Backends::InfluxDBBackend
77
+ query = ''
78
+
79
+ unless opts[:first].is_a?(TrueClass)
80
+ case @options[:op]
81
+ when :intersect, :diff
82
+ query += ' AND '
83
+ when :union
84
+ query += ' OR '
85
+ end
86
+ end
87
+
88
+ case @options[:op]
89
+ when :intersect, :union
90
+ query += @attributes.collect {|k, v|
91
+ op, value = case v
92
+ when String
93
+ ["=~", "/^#{Regexp.escape(v).gsub(/\\\\/, "\\")}$/"]
94
+ else
95
+ ["=", "'#{v}'"]
96
+ end
97
+
98
+ "#{k} #{op} #{value}"
99
+ }.join(' AND ')
100
+
101
+ when :diff
102
+ query += @attributes.collect {|k, v|
103
+ op, value = case v
104
+ when String
105
+ ["!~", "/^#{Regexp.escape(v).gsub(/\\\\/, "\\")}$/"]
106
+ else
107
+ ["!=", "'#{v}'"]
108
+ end
109
+
110
+ "#{k} #{op} #{value}"
111
+ }.join(' AND ')
112
+ else
113
+ raise "Unhandled filter operation '#{@options[:op]}'"
114
+ end
115
+
116
+ query += ")"
117
+
118
+ query
119
+ end
120
+ end
121
+
122
+ def self.evaluate(backend, op, associated_class, source, source_keys, temp_keys, opts = {})
123
+ shortcut = opts[:shortcut]
124
+
125
+ r_source_key = backend.key_to_redis_key(source)
126
+ r_source_keys = source_keys.collect {|sk| backend.key_to_redis_key(sk) }
127
+
128
+ if :ids.eql?(shortcut)
129
+ case op
130
+ when :union
131
+ backend.temp_key_wrap do |shortcut_temp_keys|
132
+ dest_set = associated_class.send(:temp_key, :set)
133
+ shortcut_temp_keys << dest_set
134
+ r_dest_set = backend.key_to_redis_key(dest_set)
135
+
136
+ Zermelo.redis.sinterstore(r_dest_set, *r_source_keys)
137
+ Zermelo.redis.sunion(r_dest_set, r_source_key)
138
+ end
139
+ when :intersect
140
+ Zermelo.redis.sinter(r_source_key, *r_source_keys)
141
+ when :diff
142
+ backend.temp_key_wrap do |shortcut_temp_keys|
143
+ dest_set = associated_class.send(:temp_key, :set)
144
+ shortcut_temp_keys << dest_set
145
+ r_dest_set = backend.key_to_redis_key(dest_set)
146
+
147
+ Zermelo.redis.sinterstore(r_dest_set, *r_source_keys)
148
+ Zermelo.redis.sdiff(r_source_key, r_dest_set)
149
+ end
150
+ end
151
+ else
152
+ dest_set = associated_class.send(:temp_key, :set)
153
+ r_dest_set = backend.key_to_redis_key(dest_set)
154
+ temp_keys << dest_set
155
+
156
+ case op
157
+ when :union
158
+ Zermelo.redis.sinterstore(r_dest_set, *r_source_keys)
159
+ Zermelo.redis.sunionstore(r_dest_set, r_source_key, r_dest_set)
160
+ when :intersect
161
+ Zermelo.redis.sinterstore(r_dest_set, r_source_key, *r_source_keys)
162
+ when :diff
163
+ Zermelo.redis.sinterstore(r_dest_set, *r_source_keys)
164
+ Zermelo.redis.sdiffstore(r_dest_set, r_source_key, r_dest_set)
165
+ end
166
+
167
+ return dest_set if shortcut.nil?
168
+ REDIS_SHORTCUTS[shortcut].call(*([r_dest_set] + opts[:shortcut_args]))
169
+ end
170
+
171
+ end
172
+
173
+ end
174
+ end
175
+ end
176
+ end
@@ -11,6 +11,91 @@ module Zermelo
11
11
  def self.returns_type
12
12
  :list
13
13
  end
14
+
15
+ def resolve(backend, associated_class, opts = {})
16
+ case backend
17
+ when Zermelo::Backends::RedisBackend
18
+ source = opts[:source]
19
+ idx_attrs = opts[:index_attrs]
20
+ attr_types = opts[:attr_types]
21
+ temp_keys = opts[:temp_keys]
22
+
23
+ dest_list = associated_class.send(:temp_key, :list)
24
+ temp_keys << dest_list
25
+ r_dest_list = backend.key_to_redis_key(dest_list)
26
+
27
+ # TODO raise error in step construction if keys not
28
+ # passed as expected below
29
+ sort_attrs_and_orders = case options[:keys]
30
+ when String, Symbol
31
+ {options[:keys].to_s => options[:desc].is_a?(TrueClass) ? :desc : :asc}
32
+ when Array
33
+ options[:keys].each_with_object({}) do |k, memo|
34
+ memo[k.to_sym] = (options[:desc].is_a?(TrueClass) ? :desc : :asc)
35
+ end
36
+ when Hash
37
+ options[:keys]
38
+ end
39
+
40
+ # TODO check if complex attribute types or associations
41
+ # can be used for sorting
42
+
43
+ r_source = backend.key_to_redis_key(source)
44
+
45
+ # this set will be overwritten by the result list
46
+ case source.type
47
+ when :set
48
+ Zermelo.redis.sunionstore(r_dest_list, r_source)
49
+ when :sorted_set
50
+ Zermelo.redis.zunionstore(r_dest_list, [r_source])
51
+ end
52
+
53
+ class_key = associated_class.send(:class_key)
54
+
55
+ sort_attrs_and_orders.keys.reverse.each_with_index do |sort_attr, idx|
56
+
57
+ order = sort_attrs_and_orders[sort_attr]
58
+
59
+ sort_opts = {}
60
+
61
+ unless 'id'.eql?(sort_attr.to_s)
62
+ sort_opts.update(:by => "#{class_key}:*:attrs->#{sort_attr}")
63
+ end
64
+
65
+ if (idx + 1) == sort_attrs_and_orders.size
66
+ # only apply offset & limit on the last sort
67
+ o = options[:offset]
68
+ l = options[:limit]
69
+
70
+ if !(l.nil? && o.nil?)
71
+ o = o.nil? ? 0 : o.to_i
72
+ l = (l.nil? || (l.to_i < 1)) ? (Zermelo.redis.llen(dest_list) - o) : l
73
+ sort_opts.update(:limit => [o, l])
74
+ end
75
+ end
76
+
77
+ order_parts = []
78
+ sort_attr_type = attr_types[sort_attr.to_sym]
79
+ unless [:integer, :float, :timestamp].include?(sort_attr_type)
80
+ order_parts << 'alpha'
81
+ end
82
+ order_parts << 'desc' if 'desc'.eql?(order.to_s)
83
+
84
+ unless order_parts.empty?
85
+ sort_opts.update(:order => order_parts.join(' '))
86
+ end
87
+
88
+ sort_opts.update(:store => r_dest_list)
89
+ Zermelo.redis.sort(r_dest_list, sort_opts)
90
+ end
91
+
92
+ shortcut = opts[:shortcut]
93
+
94
+ return dest_list if shortcut.nil?
95
+ Zermelo::Filters::Steps::ListStep::REDIS_SHORTCUTS[shortcut].
96
+ call(*([r_dest_list] + opts[:shortcut_args]))
97
+ end
98
+ end
14
99
  end
15
100
  end
16
101
  end
@@ -0,0 +1,156 @@
1
+ require 'zermelo/filters/steps/base_step'
2
+
3
+ module Zermelo
4
+ module Filters
5
+ class Steps
6
+ class SortedSetStep < Zermelo::Filters::Steps::BaseStep
7
+ def self.accepted_types
8
+ [:sorted_set]
9
+ end
10
+
11
+ def self.returns_type
12
+ :sorted_set
13
+ end
14
+
15
+ REDIS_SHORTCUTS = {
16
+ :ids => proc {|key| Zermelo.redis.zrange(key, 0, -1) },
17
+ :count => proc {|key| Zermelo.redis.zcard(key) },
18
+ :exists? => proc {|key, id| !Zermelo.redis.zscore(key, id).nil? },
19
+ :first => proc {|key| Zermelo.redis.zrange(key, 0, 0).first },
20
+ :last => proc {|key| Zermelo.redis.zrevrange(key, 0, 0).first }
21
+ }
22
+
23
+ def resolve(backend, associated_class, opts = {})
24
+ op = @options[:op]
25
+ start = @options[:start]
26
+ finish = @options[:finish]
27
+
28
+ case backend
29
+ when Zermelo::Backends::RedisBackend
30
+ source = opts[:source]
31
+ idx_attrs = opts[:index_attrs]
32
+ attr_types = opts[:attr_types]
33
+ temp_keys = opts[:temp_keys]
34
+
35
+ range_temp_key = associated_class.send(:temp_key, :sorted_set)
36
+ temp_keys << range_temp_key
37
+ range_ids_set = backend.key_to_redis_key(range_temp_key)
38
+
39
+ if @options[:by_score]
40
+ start = '-inf' if start.nil? || (start <= 0)
41
+ finish = '+inf' if finish.nil? || (finish <= 0)
42
+ else
43
+ start = 0 if start.nil?
44
+ finish = -1 if finish.nil?
45
+ end
46
+
47
+ args = [start, finish]
48
+
49
+ if @options[:by_score]
50
+ query = :zrangebyscore
51
+ args = args.map(&:to_s)
52
+ else
53
+ query = :zrange
54
+ end
55
+
56
+ args << {:with_scores => :true}
57
+
58
+ if @options[:limit]
59
+ args.last.update(:limit => [0, @options[:limit].to_i])
60
+ end
61
+
62
+ r_source = backend.key_to_redis_key(source)
63
+ args.unshift(r_source)
64
+
65
+ range_ids_scores = Zermelo.redis.send(query, *args)
66
+
67
+ unless range_ids_scores.empty?
68
+ Zermelo.redis.zadd(range_ids_set, range_ids_scores.map(&:reverse))
69
+ end
70
+
71
+ self.class.evaluate(backend, @options[:op], associated_class,
72
+ source, [range_temp_key], temp_keys, opts)
73
+
74
+ when Zermelo::Backends::InfluxDBBackend
75
+
76
+ query = ''
77
+
78
+ unless opts[:first].is_a?(TrueClass)
79
+ case @options[:op]
80
+ when :intersect_range, :diff_range
81
+ query += ' AND '
82
+ when :union_range
83
+ query += ' OR '
84
+ end
85
+ end
86
+
87
+ start = nil if !start.nil? && (start <= 0)
88
+ finish = nil if !finish.nil? && (finish <= 0)
89
+
90
+ unless start.nil? && finish.nil?
91
+ time_q = []
92
+
93
+ case @options[:op]
94
+ when :intersect_range, :union_range
95
+ unless start.nil?
96
+ time_q << "(time > #{start - 1}s)"
97
+ end
98
+ unless finish.nil?
99
+ time_q << "(time < #{finish}s)"
100
+ end
101
+ when :diff_range
102
+ unless start.nil?
103
+ time_q << "(time < #{start}s)"
104
+ end
105
+ unless finish.nil?
106
+ time_q << "(time > #{finish - 1}s)"
107
+ end
108
+ end
109
+
110
+ query += time_q.join(' AND ')
111
+ end
112
+
113
+ query += ")"
114
+ query
115
+ end
116
+ end
117
+
118
+ def self.evaluate(backend, op, associated_class, source, source_keys, temp_keys, opts = {})
119
+ shortcut = opts[:shortcut]
120
+
121
+ weights = case op
122
+ when :union, :union_range
123
+ [0.0] * source_keys.length
124
+ when :diff, :diff_range
125
+ [-1.0] * source_keys.length
126
+ end
127
+
128
+ r_source = backend.key_to_redis_key(source)
129
+ r_source_keys = source_keys.collect {|sk| backend.key_to_redis_key(sk) }
130
+
131
+ dest_sorted_set = associated_class.send(:temp_key, :sorted_set)
132
+ temp_keys << dest_sorted_set
133
+ r_dest_sorted_set = backend.key_to_redis_key(dest_sorted_set)
134
+
135
+ case op
136
+ when :union, :union_range
137
+ Zermelo.redis.zinterstore(r_dest_sorted_set, r_source_keys, :weights => weights, :aggregate => 'max')
138
+ Zermelo.redis.zunionstore(r_dest_sorted_set, [r_source, r_dest_sorted_set])
139
+ when :intersect, :intersect_range
140
+ Zermelo.redis.zinterstore(r_dest_sorted_set, [r_source] + r_source_keys, :weights => weights, :aggregate => 'max')
141
+ when :diff, :diff_range
142
+ # 'zdiffstore' via weights, relies on non-zero scores being used
143
+ # see https://code.google.com/p/redis/issues/detail?id=579
144
+ Zermelo.redis.zinterstore(r_dest_sorted_set, r_source_keys, :weights => weights, :aggregate => 'max')
145
+ Zermelo.redis.zunionstore(r_dest_sorted_set, [r_source, r_dest_sorted_set])
146
+ Zermelo.redis.zremrangebyscore(r_dest_sorted_set, "0", "0")
147
+ end
148
+
149
+ return dest_sorted_set if shortcut.nil?
150
+ REDIS_SHORTCUTS[shortcut].call(*([r_dest_sorted_set] + opts[:shortcut_args]))
151
+ end
152
+
153
+ end
154
+ end
155
+ end
156
+ end
@@ -102,13 +102,25 @@ module Zermelo
102
102
 
103
103
  private
104
104
 
105
+ def class_key
106
+ self.name.demodulize.underscore
107
+ end
108
+
105
109
  def ids_key
106
- @ids_key ||= Zermelo::Records::Key.new(:klass => class_key, :name => 'ids',
107
- :type => :set, :object => :attribute)
110
+ @ids_key ||= Zermelo::Records::Key.new(
111
+ :klass => self, :name => 'ids',
112
+ :type => :set,
113
+ :object => :attribute
114
+ )
108
115
  end
109
116
 
110
- def class_key
111
- self.name.demodulize.underscore
117
+ def temp_key(type)
118
+ Zermelo::Records::Key.new(
119
+ :klass => self,
120
+ :name => SecureRandom.hex(16),
121
+ :type => type,
122
+ :object => :temporary
123
+ )
112
124
  end
113
125
 
114
126
  def load(id)
@@ -28,6 +28,8 @@ module Zermelo
28
28
 
29
29
  included do
30
30
  set_backend :influxdb
31
+
32
+ define_attributes :time => :timestamp
31
33
  end
32
34
 
33
35
  end
@@ -51,7 +51,7 @@ module Zermelo
51
51
  attr_types = self.class.attribute_types.reject {|k, v| k == :id}
52
52
 
53
53
  attrs_to_load = attr_types.collect do |name, type|
54
- Zermelo::Records::Key.new(:klass => class_key,
54
+ Zermelo::Records::Key.new(:klass => self.class,
55
55
  :id => self.id, :name => name, :type => type, :object => :attribute)
56
56
  end
57
57
 
@@ -167,12 +167,12 @@ module Zermelo
167
167
  end
168
168
 
169
169
  self.class.attribute_types.each_pair {|name, type|
170
- key = Zermelo::Records::Key.new(:klass => self.class.send(:class_key),
170
+ key = Zermelo::Records::Key.new(:klass => self.class,
171
171
  :id => self.id, :name => name.to_s, :type => type, :object => :attribute)
172
172
  backend.clear(key)
173
173
  }
174
174
 
175
- record_key = Zermelo::Records::Key.new(:klass => self.class.send(:class_key),
175
+ record_key = Zermelo::Records::Key.new(:klass => self.class,
176
176
  :id => self.id)
177
177
  backend.purge(record_key)
178
178
  end
@@ -190,7 +190,7 @@ module Zermelo
190
190
  @attribute_keys ||= self.class.attribute_types.reject {|k, v|
191
191
  k == :id
192
192
  }.inject({}) {|memo, (name, type)|
193
- memo[name.to_s] = Zermelo::Records::Key.new(:klass => self.class.send(:class_key),
193
+ memo[name.to_s] = Zermelo::Records::Key.new(:klass => self.class,
194
194
  :id => self.id, :name => name.to_s, :type => type, :object => :attribute)
195
195
  memo
196
196
  }
@@ -1,5 +1,6 @@
1
1
  module Zermelo
2
2
  module Records
3
+
3
4
  class Key
4
5
 
5
6
  # id / if nil, it's a class variable
@@ -15,5 +16,6 @@ module Zermelo
15
16
  end
16
17
 
17
18
  end
19
+
18
20
  end
19
21
  end
@@ -1,3 +1,3 @@
1
1
  module Zermelo
2
- VERSION = '1.0.1'
2
+ VERSION = '1.1.0'
3
3
  end
data/lib/zermelo.rb CHANGED
@@ -95,7 +95,15 @@ module Zermelo
95
95
  debug_str
96
96
  }
97
97
  end
98
- @proxied_connection.send(name, *args, &block)
98
+ result = @proxied_connection.send(name, *args, &block)
99
+ unless Zermelo.logger.nil?
100
+ Zermelo.logger.debug {
101
+ debug_str = "#{name}"
102
+ debug_str += " result: #{result}"
103
+ debug_str
104
+ }
105
+ end
106
+ result
99
107
  end
100
108
  end
101
109