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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +10 -0
- data/.travis.yml +27 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +512 -0
- data/Rakefile +1 -0
- data/lib/zermelo/associations/association_data.rb +24 -0
- data/lib/zermelo/associations/belongs_to.rb +115 -0
- data/lib/zermelo/associations/class_methods.rb +244 -0
- data/lib/zermelo/associations/has_and_belongs_to_many.rb +128 -0
- data/lib/zermelo/associations/has_many.rb +120 -0
- data/lib/zermelo/associations/has_one.rb +109 -0
- data/lib/zermelo/associations/has_sorted_set.rb +124 -0
- data/lib/zermelo/associations/index.rb +50 -0
- data/lib/zermelo/associations/index_data.rb +18 -0
- data/lib/zermelo/associations/unique_index.rb +44 -0
- data/lib/zermelo/backends/base.rb +115 -0
- data/lib/zermelo/backends/influxdb_backend.rb +178 -0
- data/lib/zermelo/backends/redis_backend.rb +281 -0
- data/lib/zermelo/filters/base.rb +235 -0
- data/lib/zermelo/filters/influxdb_filter.rb +162 -0
- data/lib/zermelo/filters/redis_filter.rb +558 -0
- data/lib/zermelo/filters/steps/base_step.rb +22 -0
- data/lib/zermelo/filters/steps/diff_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/diff_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_step.rb +17 -0
- data/lib/zermelo/filters/steps/limit_step.rb +17 -0
- data/lib/zermelo/filters/steps/offset_step.rb +17 -0
- data/lib/zermelo/filters/steps/sort_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_step.rb +17 -0
- data/lib/zermelo/locks/no_lock.rb +16 -0
- data/lib/zermelo/locks/redis_lock.rb +221 -0
- data/lib/zermelo/records/base.rb +62 -0
- data/lib/zermelo/records/class_methods.rb +127 -0
- data/lib/zermelo/records/collection.rb +14 -0
- data/lib/zermelo/records/errors.rb +24 -0
- data/lib/zermelo/records/influxdb_record.rb +35 -0
- data/lib/zermelo/records/instance_methods.rb +224 -0
- data/lib/zermelo/records/key.rb +19 -0
- data/lib/zermelo/records/redis_record.rb +27 -0
- data/lib/zermelo/records/type_validator.rb +20 -0
- data/lib/zermelo/version.rb +3 -0
- data/lib/zermelo.rb +102 -0
- data/spec/lib/zermelo/associations/belongs_to_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_many_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_one_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +6 -0
- data/spec/lib/zermelo/associations/index_spec.rb +6 -0
- data/spec/lib/zermelo/associations/unique_index_spec.rb +6 -0
- data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
- data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
- data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
- data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
- data/spec/lib/zermelo/locks/redis_lock_spec.rb +170 -0
- data/spec/lib/zermelo/records/influxdb_record_spec.rb +258 -0
- data/spec/lib/zermelo/records/key_spec.rb +6 -0
- data/spec/lib/zermelo/records/redis_record_spec.rb +1426 -0
- data/spec/lib/zermelo/records/type_validator_spec.rb +6 -0
- data/spec/lib/zermelo/version_spec.rb +6 -0
- data/spec/lib/zermelo_spec.rb +6 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/support/profile_all_formatter.rb +44 -0
- data/spec/support/uncolored_doc_formatter.rb +74 -0
- data/zermelo.gemspec +30 -0
- 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
|