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