zermelo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +10 -0
  4. data/.travis.yml +27 -0
  5. data/Gemfile +20 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +512 -0
  8. data/Rakefile +1 -0
  9. data/lib/zermelo/associations/association_data.rb +24 -0
  10. data/lib/zermelo/associations/belongs_to.rb +115 -0
  11. data/lib/zermelo/associations/class_methods.rb +244 -0
  12. data/lib/zermelo/associations/has_and_belongs_to_many.rb +128 -0
  13. data/lib/zermelo/associations/has_many.rb +120 -0
  14. data/lib/zermelo/associations/has_one.rb +109 -0
  15. data/lib/zermelo/associations/has_sorted_set.rb +124 -0
  16. data/lib/zermelo/associations/index.rb +50 -0
  17. data/lib/zermelo/associations/index_data.rb +18 -0
  18. data/lib/zermelo/associations/unique_index.rb +44 -0
  19. data/lib/zermelo/backends/base.rb +115 -0
  20. data/lib/zermelo/backends/influxdb_backend.rb +178 -0
  21. data/lib/zermelo/backends/redis_backend.rb +281 -0
  22. data/lib/zermelo/filters/base.rb +235 -0
  23. data/lib/zermelo/filters/influxdb_filter.rb +162 -0
  24. data/lib/zermelo/filters/redis_filter.rb +558 -0
  25. data/lib/zermelo/filters/steps/base_step.rb +22 -0
  26. data/lib/zermelo/filters/steps/diff_range_step.rb +17 -0
  27. data/lib/zermelo/filters/steps/diff_step.rb +17 -0
  28. data/lib/zermelo/filters/steps/intersect_range_step.rb +17 -0
  29. data/lib/zermelo/filters/steps/intersect_step.rb +17 -0
  30. data/lib/zermelo/filters/steps/limit_step.rb +17 -0
  31. data/lib/zermelo/filters/steps/offset_step.rb +17 -0
  32. data/lib/zermelo/filters/steps/sort_step.rb +17 -0
  33. data/lib/zermelo/filters/steps/union_range_step.rb +17 -0
  34. data/lib/zermelo/filters/steps/union_step.rb +17 -0
  35. data/lib/zermelo/locks/no_lock.rb +16 -0
  36. data/lib/zermelo/locks/redis_lock.rb +221 -0
  37. data/lib/zermelo/records/base.rb +62 -0
  38. data/lib/zermelo/records/class_methods.rb +127 -0
  39. data/lib/zermelo/records/collection.rb +14 -0
  40. data/lib/zermelo/records/errors.rb +24 -0
  41. data/lib/zermelo/records/influxdb_record.rb +35 -0
  42. data/lib/zermelo/records/instance_methods.rb +224 -0
  43. data/lib/zermelo/records/key.rb +19 -0
  44. data/lib/zermelo/records/redis_record.rb +27 -0
  45. data/lib/zermelo/records/type_validator.rb +20 -0
  46. data/lib/zermelo/version.rb +3 -0
  47. data/lib/zermelo.rb +102 -0
  48. data/spec/lib/zermelo/associations/belongs_to_spec.rb +6 -0
  49. data/spec/lib/zermelo/associations/has_many_spec.rb +6 -0
  50. data/spec/lib/zermelo/associations/has_one_spec.rb +6 -0
  51. data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +6 -0
  52. data/spec/lib/zermelo/associations/index_spec.rb +6 -0
  53. data/spec/lib/zermelo/associations/unique_index_spec.rb +6 -0
  54. data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
  55. data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
  56. data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
  57. data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
  58. data/spec/lib/zermelo/locks/redis_lock_spec.rb +170 -0
  59. data/spec/lib/zermelo/records/influxdb_record_spec.rb +258 -0
  60. data/spec/lib/zermelo/records/key_spec.rb +6 -0
  61. data/spec/lib/zermelo/records/redis_record_spec.rb +1426 -0
  62. data/spec/lib/zermelo/records/type_validator_spec.rb +6 -0
  63. data/spec/lib/zermelo/version_spec.rb +6 -0
  64. data/spec/lib/zermelo_spec.rb +6 -0
  65. data/spec/spec_helper.rb +67 -0
  66. data/spec/support/profile_all_formatter.rb +44 -0
  67. data/spec/support/uncolored_doc_formatter.rb +74 -0
  68. data/zermelo.gemspec +30 -0
  69. metadata +174 -0
@@ -0,0 +1,109 @@
1
+ module Zermelo
2
+ module Associations
3
+ class HasOne
4
+
5
+ def initialize(parent, name)
6
+ @parent = parent
7
+
8
+ @backend = parent.send(:backend)
9
+
10
+ # TODO would be better as a 'has_one' hash, a bit like belongs_to
11
+ @record_id_key = Zermelo::Records::Key.new(
12
+ :klass => parent.class.send(:class_key),
13
+ :id => parent.id,
14
+ :name => "#{name}_id",
15
+ :type => :string,
16
+ :object => :association
17
+ )
18
+
19
+ parent.class.send(:with_association_data, name.to_sym) do |data|
20
+ @associated_class = data.data_klass
21
+ @lock_klasses = [data.data_klass] + data.related_klasses
22
+ @inverse = data.inverse
23
+ @callbacks = data.callbacks
24
+ end
25
+ end
26
+
27
+ def value
28
+ @parent.class.lock(*@associated_class) do
29
+ if id = @backend.get(@record_id_key)
30
+ @associated_class.send(:load, id)
31
+ else
32
+ nil
33
+ end
34
+ end
35
+ end
36
+
37
+ def value=(record)
38
+ if record.nil?
39
+ @parent.class.lock(*@lock_klasses) do
40
+ id = @backend.get(@record_id_key)
41
+ unless id.nil?
42
+ r = @associated_class.send(:load, id)
43
+ unless r.nil?
44
+ bc = @callbacks[:before_clear]
45
+ if bc.nil? || !@parent.respond_to?(bc) || !@parent.send(bc, r).is_a?(FalseClass)
46
+ delete_without_lock(r)
47
+ ac = @callbacks[:after_clear]
48
+ @parent.send(ac, r) if !ac.nil? && @parent.respond_to?(ac)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ else
54
+ raise 'Invalid record class' unless record.is_a?(@associated_class)
55
+ raise 'Record must have been saved' unless record.persisted?
56
+ @parent.class.lock(*@lock_klasses) do
57
+ bs = @callbacks[:before_set]
58
+ if bs.nil? || !@parent.respond_to?(bs) || !@parent.send(bs, r).is_a?(FalseClass)
59
+ unless @inverse.nil?
60
+ @associated_class.send(:load, record.id).send("#{@inverse}=", @parent)
61
+ end
62
+
63
+ new_txn = @backend.begin_transaction
64
+ @backend.set(@record_id_key, record.id)
65
+ @backend.commit_transaction if new_txn
66
+ as = @callbacks[:after_set]
67
+ @parent.send(as, record) if !as.nil? && @parent.respond_to?(as)
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def delete_without_lock(record)
76
+ unless @inverse.nil?
77
+ record.send("#{@inverse}=", nil)
78
+ end
79
+ new_txn = @backend.begin_transaction
80
+ @backend.clear(@record_id_key)
81
+ @backend.commit_transaction if new_txn
82
+ end
83
+
84
+ # associated will be a belongs_to; on_remove already runs inside lock and transaction
85
+ def on_remove
86
+ unless @inverse.nil?
87
+ if record_id = @backend.get(@record_id_key)
88
+ @associated_class.send(:load, record_id).send("#{@inverse}=", nil)
89
+ end
90
+ end
91
+ @backend.clear(@record_id_key)
92
+ end
93
+
94
+ def self.associated_ids_for(backend, class_key, name, *these_ids)
95
+ these_ids.each_with_object({}) do |this_id, memo|
96
+ key = Zermelo::Records::Key.new(
97
+ :klass => class_key,
98
+ :id => this_id,
99
+ :name => "#{name}_id",
100
+ :type => :string,
101
+ :object => :association
102
+ )
103
+ memo[this_id] = backend.get(key)
104
+ end
105
+ end
106
+
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,124 @@
1
+ require 'forwardable'
2
+
3
+ module Zermelo
4
+ module Associations
5
+ class HasSortedSet
6
+
7
+ extend Forwardable
8
+
9
+ def_delegators :filter, :intersect, :union, :diff, :sort,
10
+ :intersect_range, :union_range, :diff_range,
11
+ :find_by_id, :find_by_ids, :find_by_id!, :find_by_ids!,
12
+ :all, :each, :collect, :map,
13
+ :select, :find_all, :reject, :destroy_all,
14
+ :ids, :count, :empty?, :exists?,
15
+ :first, :last,
16
+ :associated_ids_for
17
+
18
+ def initialize(parent, name)
19
+ @parent = parent
20
+
21
+ @backend = parent.send(:backend)
22
+
23
+ @record_ids_key = Zermelo::Records::Key.new(
24
+ :klass => parent.class.send(:class_key),
25
+ :id => parent.id,
26
+ :name => "#{name}_ids",
27
+ :type => :sorted_set,
28
+ :object => :association
29
+ )
30
+
31
+ parent.class.send(:with_association_data, name.to_sym) do |data|
32
+ @associated_class = data.data_klass
33
+ @lock_klasses = [data.data_klass] + data.related_klasses
34
+ @inverse = data.inverse
35
+ @sort_key = data.sort_key
36
+ @callbacks = data.callbacks
37
+ end
38
+ end
39
+
40
+ def <<(record)
41
+ add(record)
42
+ self # for << 'a' << 'b'
43
+ end
44
+
45
+ # TODO collect all scores/ids and do a single zadd/single hmset
46
+ def add(*records)
47
+ raise 'No records to add' if records.empty?
48
+ raise 'Invalid record class' if records.any? {|r| !r.is_a?(@associated_class)}
49
+ raise 'Record(s) must have been saved' unless records.all? {|r| r.persisted?}
50
+ @parent.class.lock(*@lock_klasses) do
51
+ ba = @callbacks[:before_add]
52
+ if ba.nil? || !@parent.respond_to?(ba) || !@parent.send(ba, *records).is_a?(FalseClass)
53
+ unless @inverse.nil?
54
+ records.each do |record|
55
+ @associated_class.send(:load, record.id).send("#{@inverse}=", @parent)
56
+ end
57
+ end
58
+
59
+ new_txn = @backend.begin_transaction
60
+ @backend.add(@record_ids_key, (records.map {|r| [r.send(@sort_key.to_sym).to_f, r.id]}.flatten))
61
+ @backend.commit_transaction if new_txn
62
+ aa = @callbacks[:after_add]
63
+ @parent.send(aa, *records) if !aa.nil? && @parent.respond_to?(aa)
64
+ end
65
+ end
66
+ end
67
+
68
+ def delete(*records)
69
+ raise 'No records to delete' if records.empty?
70
+ raise 'Invalid record class' if records.any? {|r| !r.is_a?(@associated_class)}
71
+ raise 'Record(s) must have been saved' unless records.all? {|r| r.persisted?}
72
+ @parent.class.lock(*@lock_klasses) do
73
+ br = @callbacks[:before_remove]
74
+ if br.nil? || !@parent.respond_to?(br) || !@parent.send(br, *records).is_a?(FalseClass)
75
+ unless @inverse.nil?
76
+ records.each do |record|
77
+ @associated_class.send(:load, record.id).send("#{@inverse}=", nil)
78
+ end
79
+ end
80
+
81
+ new_txn = @backend.begin_transaction
82
+ @backend.delete(@record_ids_key, records.map(&:id))
83
+ @backend.commit_transaction if new_txn
84
+ ar = @callbacks[:after_remove]
85
+ @parent.send(ar, *records) if !ar.nil? && @parent.respond_to?(ar)
86
+ end
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # associated will be a belongs_to; on remove already runs inside a lock and transaction
93
+ def on_remove
94
+ unless @inverse.nil?
95
+ self.ids.each do |record_id|
96
+ # clear the belongs_to inverse value with this @parent.id
97
+ @associated_class.send(:load, record_id).send("#{@inverse}=", nil)
98
+ end
99
+ end
100
+ @backend.clear(@record_ids_key)
101
+ end
102
+
103
+ # creates a new filter class each time it's called, to store the
104
+ # state for this particular filter chain
105
+ def filter
106
+ @backend.filter(@record_ids_key, @associated_class)
107
+ end
108
+
109
+ def self.associated_ids_for(backend, class_key, name, *these_ids)
110
+ these_ids.each_with_object({}) do |this_id, memo|
111
+ key = Zermelo::Records::Key.new(
112
+ :klass => class_key,
113
+ :id => this_id,
114
+ :name => "#{name}_ids",
115
+ :type => :sorted_set,
116
+ :object => :association
117
+ )
118
+ memo[this_id] = backend.get(key)
119
+ end
120
+ end
121
+
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,50 @@
1
+ # NB index instances are all internal to zermelo, not user-accessible
2
+
3
+ module Zermelo
4
+ module Associations
5
+ class Index
6
+
7
+ def initialize(parent_klass, name)
8
+ @parent_klass = parent_klass
9
+ @attribute_name = name
10
+
11
+ @backend = parent_klass.send(:backend)
12
+ @class_key = parent_klass.send(:class_key)
13
+
14
+ @indexers = {}
15
+
16
+ parent_klass.send(:with_index_data, name.to_sym) do |data|
17
+ @attribute_type = data.type
18
+ end
19
+ end
20
+
21
+ def delete_id(id, value)
22
+ return unless indexer = key(value)
23
+ @backend.delete(indexer, id)
24
+ end
25
+
26
+ def add_id(id, value)
27
+ return unless indexer = key(value)
28
+ @backend.add(indexer, id)
29
+ end
30
+
31
+ def move_id(id, value_from, indexer_to, value_to)
32
+ return unless indexer = key(value_from)
33
+ @backend.move(indexer, id, indexer_to.key(value_to))
34
+ end
35
+
36
+ def key(value)
37
+ index_keys = @backend.index_keys(@attribute_type, value)
38
+ raise "Can't index '#{@value}' (#{@attribute_type}" if index_keys.nil?
39
+
40
+ @indexers[index_keys.join(":")] ||= Zermelo::Records::Key.new(
41
+ :klass => @class_key,
42
+ :name => "by_#{@attribute_name}:#{index_keys.join(':')}",
43
+ :type => :set,
44
+ :object => :index
45
+ )
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ module Zermelo
2
+ module Associations
3
+ class IndexData
4
+ attr_writer :data_klass_name
5
+ attr_accessor :name, :type, :index_klass
6
+
7
+ def initialize(opts = {})
8
+ [:name, :type, :index_klass].each do |a|
9
+ send("#{a}=".to_sym, opts[a])
10
+ end
11
+ end
12
+
13
+ def data_klass
14
+ @data_klass ||= @data_klass_name.constantize
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # NB index instances are all internal to zermelo, not user-accessible
2
+
3
+ module Zermelo
4
+ module Associations
5
+ class UniqueIndex
6
+
7
+ def initialize(parent_klass, name)
8
+ @parent_klass = parent_klass
9
+ @attribute_name = name
10
+
11
+ @backend = parent_klass.send(:backend)
12
+ @class_key = parent_klass.send(:class_key)
13
+
14
+ @indexers = {}
15
+
16
+ parent_klass.send(:with_index_data, name.to_sym) do |data|
17
+ @attribute_type = data.type
18
+ end
19
+ end
20
+
21
+ def delete_id(id, value)
22
+ @backend.delete(key, @backend.index_keys(@attribute_type, value).join(':'))
23
+ end
24
+
25
+ def add_id(id, value)
26
+ @backend.add(key, @backend.index_keys(@attribute_type, value).join(':') => id)
27
+ end
28
+
29
+ def move_id(id, value_from, indexer_to, value_to)
30
+ @backend.move(key, {@backend.index_keys(@attribute_type, value_to).join(':') => id}, indexer_to.key)
31
+ end
32
+
33
+ def key
34
+ @indexer ||= Zermelo::Records::Key.new(
35
+ :klass => @class_key,
36
+ :name => "by_#{@attribute_name}",
37
+ :type => :hash,
38
+ :object => :index
39
+ )
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,115 @@
1
+ require 'active_support/concern'
2
+
3
+ require 'zermelo/locks/no_lock'
4
+
5
+ module Zermelo
6
+
7
+ module Backends
8
+
9
+ module Base
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ def escape_key_name(name)
14
+ name.gsub(/%/, '%%').gsub(/ /, '%20').gsub(/:/, '%3A')
15
+ end
16
+
17
+ def unescape_key_name(name)
18
+ name.gsub(/%3A/, ':').gsub(/%20/, ' ').gsub(/%%/, '%')
19
+ end
20
+
21
+ def index_keys(type, value)
22
+ return ["null", "null"] if value.nil?
23
+
24
+ case type
25
+ when :string
26
+ ["string", escape_key_name(value)]
27
+ when :integer
28
+ ["integer", escape_key_name(value.to_s)]
29
+ when :float
30
+ ["float", escape_key_name(value.to_s)]
31
+ when :timestamp
32
+ case value
33
+ when Integer
34
+ ["timestamp", escape_key_name(value.to_s)]
35
+ when Time, DateTime
36
+ ["timestamp", escape_key_name(value.to_i.to_s)]
37
+ end
38
+ when :boolean
39
+ case value
40
+ when TrueClass
41
+ ["boolean", "true"]
42
+ when FalseClass
43
+ ["boolean", "false"]
44
+ end
45
+ end
46
+ end
47
+
48
+ # for hashes, lists, sets
49
+ def add(key, value)
50
+ change(:add, key, value)
51
+ end
52
+
53
+ def delete(key, value)
54
+ change(:delete, key, value)
55
+ end
56
+
57
+ def move(key, value, key_to)
58
+ change(:move, key, value, key_to)
59
+ end
60
+
61
+ def clear(key)
62
+ change(:clear, key)
63
+ end
64
+
65
+ # works for both simple and complex types (i.e. strings, numbers, booleans,
66
+ # hashes, lists, sets)
67
+ def set(key, value)
68
+ change(:set, key, value)
69
+ end
70
+
71
+ def purge(key)
72
+ change(:purge, key)
73
+ end
74
+
75
+ def get(attr_key)
76
+ get_multiple(attr_key)[attr_key.klass][attr_key.id][attr_key.name.to_s]
77
+ end
78
+
79
+ def lock(*klasses, &block)
80
+ ret = nil
81
+ # doesn't handle re-entrant case for influxdb, which has no locking yet
82
+ locking = Thread.current[:zermelo_locking]
83
+ if locking.nil?
84
+ lock_proc = proc do
85
+ begin
86
+ Thread.current[:zermelo_locking] = klasses
87
+ ret = block.call
88
+ ensure
89
+ Thread.current[:zermelo_locking] = nil
90
+ end
91
+ end
92
+
93
+ lock_klass = case self
94
+ when Zermelo::Backends::RedisBackend
95
+ Zermelo::Locks::RedisLock
96
+ else
97
+ Zermelo::Locks::NoLock
98
+ end
99
+
100
+ lock_klass.new.lock(*klasses, &lock_proc)
101
+ else
102
+ # accepts any subset of 'locking'
103
+ unless (klasses - locking).empty?
104
+ raise "Currently locking #{locking.map(&:name)}, cannot lock different set #{klasses.map(&:name)}"
105
+ end
106
+ ret = block.call
107
+ end
108
+ ret
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+
115
+ end
@@ -0,0 +1,178 @@
1
+ require 'zermelo/backends/base'
2
+
3
+ require 'zermelo/filters/influxdb_filter'
4
+
5
+ # NB influxdb doesn't support individually addressable deletes, so
6
+ # this backend only works to write new records
7
+ # (it could just write the new state of the record, and query id by newest limit 1,
8
+ # but for the moment, YAGNI)
9
+
10
+ module Zermelo
11
+
12
+ module Backends
13
+
14
+ class InfluxDBBackend
15
+
16
+ include Zermelo::Backends::Base
17
+
18
+ def filter(ids_key, record)
19
+ Zermelo::Filters::InfluxDBFilter.new(self, ids_key, record)
20
+ end
21
+
22
+ # TODO get filter calling this instead of using same logic
23
+ def exists?(key)
24
+ return if key.id.nil?
25
+ Zermelo.influxdb.query("SELECT id FROM /#{key.klass}\\/.*/ LIMIT 1").size > 0
26
+ end
27
+
28
+ # nb: does lots of queries, should batch, but ensuring single operations are correct
29
+ # for now
30
+ def get_multiple(*attr_keys)
31
+ attr_keys.inject({}) do |memo, attr_key|
32
+ begin
33
+ records = Zermelo.influxdb.query("SELECT #{attr_key.name} FROM " +
34
+ "\"#{attr_key.klass}/#{attr_key.id}\" LIMIT 1")["#{attr_key.klass}/#{attr_key.id}"]
35
+ rescue InfluxDB::Error => ide
36
+ raise unless
37
+ /^Field #{attr_key.name} doesn't exist in series #{attr_key.klass}\/#{attr_key.id}$/ === ide.message
38
+
39
+ records = []
40
+ end
41
+ value = (records && !records.empty?) ? records.first[attr_key.name.to_s] : nil
42
+
43
+ memo[attr_key.klass] ||= {}
44
+ memo[attr_key.klass][attr_key.id] ||= {}
45
+
46
+ memo[attr_key.klass][attr_key.id][attr_key.name.to_s] = if value.nil?
47
+ nil
48
+ else
49
+
50
+ case attr_key.type
51
+ when :string
52
+ value.to_s
53
+ when :integer
54
+ value.to_i
55
+ when :float
56
+ value.to_f
57
+ when :timestamp
58
+ Time.at(value.to_f)
59
+ when :boolean
60
+ case value
61
+ when TrueClass
62
+ true
63
+ when FalseClass
64
+ false
65
+ when String
66
+ 'true'.eql?(value.downcase)
67
+ else
68
+ nil
69
+ end
70
+ when :list, :hash
71
+ value
72
+ when :set
73
+ Set.new(value)
74
+ end
75
+ end
76
+ memo
77
+ end
78
+ end
79
+
80
+ def begin_transaction
81
+ return false if @in_transaction
82
+ @in_transaction = true
83
+ @changes = []
84
+ end
85
+
86
+ def commit_transaction
87
+ return false unless @in_transaction
88
+ apply_changes(@changes)
89
+ @in_transaction = false
90
+ @changes = []
91
+ end
92
+
93
+ def abort_transaction
94
+ return false unless @in_transaction
95
+ @in_transaction = false
96
+ @changes = []
97
+ end
98
+
99
+ private
100
+
101
+ def change(op, key, value = nil)
102
+ ch = [op, key, value]
103
+ if @in_transaction
104
+ @changes << ch
105
+ else
106
+ apply_changes([ch])
107
+ end
108
+ end
109
+
110
+ # composite all new changes into records, and then into influxdb
111
+ # query statements
112
+ def apply_changes(changes)
113
+ records = {}
114
+
115
+ purges = []
116
+
117
+ changes.each do |ch|
118
+ op = ch[0]
119
+ key = ch[1]
120
+ value = ch[2]
121
+
122
+ next if key.id.nil?
123
+
124
+ records[key.klass] ||= {}
125
+ records[key.klass][key.id] ||= {}
126
+
127
+ records[key.klass][key.id][key.name] = case op
128
+ when :set
129
+ case key.type
130
+ when :string, :integer
131
+ value.to_s
132
+ when :timestamp
133
+ value.to_f
134
+ when :boolean
135
+ (!!value).to_s
136
+ when :list, :hash
137
+ value
138
+ when :set
139
+ value.to_a
140
+ end
141
+ when :add
142
+ case key.type
143
+ when :list, :hash
144
+ value
145
+ when :set
146
+ value.to_a
147
+ end
148
+ when :purge
149
+ purges << "\"#{key.klass}/#{key.id}\""
150
+ end
151
+
152
+ end
153
+
154
+ records.each_pair do |klass, klass_records|
155
+ klass_records.each_pair do |id, data|
156
+ begin
157
+ prior = Zermelo.influxdb.query("SELECT * FROM \"#{klass}/#{id}\" LIMIT 1")["#{klass}/#{id}"]
158
+ rescue InfluxDB::Error => ide
159
+ raise unless
160
+ (/^Couldn't look up columns for series: #{klass}\/#{id}$/ === ide.message) ||
161
+ (/^Couldn't look up columns$/ === ide.message) ||
162
+ (/^Couldn't find series: #{klass}\/#{id}$/ === ide.message)
163
+
164
+ prior = nil
165
+ end
166
+ record = prior.nil? ? {} : prior.first.delete_if {|k,v| ["time", "sequence_number"].include?(k) }
167
+ Zermelo.influxdb.write_point("#{klass}/#{id}", record.merge(data).merge('id' => id))
168
+ end
169
+ end
170
+
171
+ purges.each {|purge| Zermelo.influxdb.query("DROP SERIES #{purge}") }
172
+ end
173
+
174
+ end
175
+
176
+ end
177
+
178
+ end