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
@@ -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