zermelo 1.1.0 → 1.2.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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +76 -52
- data/lib/zermelo/associations/association_data.rb +4 -3
- data/lib/zermelo/associations/class_methods.rb +37 -50
- data/lib/zermelo/associations/index.rb +3 -1
- data/lib/zermelo/associations/multiple.rb +247 -0
- data/lib/zermelo/associations/range_index.rb +44 -0
- data/lib/zermelo/associations/singular.rb +193 -0
- data/lib/zermelo/associations/unique_index.rb +4 -3
- data/lib/zermelo/backend.rb +120 -0
- data/lib/zermelo/backends/{influxdb_backend.rb → influxdb.rb} +87 -31
- data/lib/zermelo/backends/{redis_backend.rb → redis.rb} +53 -58
- data/lib/zermelo/backends/stub.rb +43 -0
- data/lib/zermelo/filter.rb +194 -0
- data/lib/zermelo/filters/index_range.rb +22 -0
- data/lib/zermelo/filters/{influxdb_filter.rb → influxdb.rb} +12 -11
- data/lib/zermelo/filters/redis.rb +173 -0
- data/lib/zermelo/filters/steps/list_step.rb +48 -30
- data/lib/zermelo/filters/steps/set_step.rb +148 -89
- data/lib/zermelo/filters/steps/sort_step.rb +2 -2
- data/lib/zermelo/record.rb +53 -0
- data/lib/zermelo/records/attributes.rb +32 -0
- data/lib/zermelo/records/class_methods.rb +12 -25
- data/lib/zermelo/records/{influxdb_record.rb → influxdb.rb} +3 -4
- data/lib/zermelo/records/instance_methods.rb +9 -8
- data/lib/zermelo/records/key.rb +3 -1
- data/lib/zermelo/records/redis.rb +17 -0
- data/lib/zermelo/records/stub.rb +17 -0
- data/lib/zermelo/version.rb +1 -1
- data/spec/lib/zermelo/associations/index_spec.rb +70 -1
- data/spec/lib/zermelo/associations/multiple_spec.rb +1084 -0
- data/spec/lib/zermelo/associations/range_index_spec.rb +77 -0
- data/spec/lib/zermelo/associations/singular_spec.rb +149 -0
- data/spec/lib/zermelo/associations/unique_index_spec.rb +58 -2
- data/spec/lib/zermelo/filter_spec.rb +363 -0
- data/spec/lib/zermelo/locks/redis_lock_spec.rb +3 -3
- data/spec/lib/zermelo/records/instance_methods_spec.rb +206 -0
- data/spec/spec_helper.rb +9 -1
- data/spec/support/mock_logger.rb +48 -0
- metadata +31 -46
- data/lib/zermelo/associations/belongs_to.rb +0 -115
- data/lib/zermelo/associations/has_and_belongs_to_many.rb +0 -128
- data/lib/zermelo/associations/has_many.rb +0 -120
- data/lib/zermelo/associations/has_one.rb +0 -109
- data/lib/zermelo/associations/has_sorted_set.rb +0 -124
- data/lib/zermelo/backends/base.rb +0 -115
- data/lib/zermelo/filters/base.rb +0 -212
- data/lib/zermelo/filters/redis_filter.rb +0 -111
- data/lib/zermelo/filters/steps/sorted_set_step.rb +0 -156
- data/lib/zermelo/records/base.rb +0 -62
- data/lib/zermelo/records/redis_record.rb +0 -27
- data/spec/lib/zermelo/associations/belongs_to_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_many_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_one_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +0 -6
- 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/records/influxdb_record_spec.rb +0 -434
- data/spec/lib/zermelo/records/key_spec.rb +0 -6
- data/spec/lib/zermelo/records/redis_record_spec.rb +0 -1461
- data/spec/lib/zermelo/records/type_validator_spec.rb +0 -6
- data/spec/lib/zermelo/version_spec.rb +0 -6
- data/spec/lib/zermelo_spec.rb +0 -6
@@ -0,0 +1,247 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Zermelo
|
4
|
+
module Associations
|
5
|
+
class Multiple
|
6
|
+
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :filter, :intersect, :union, :diff, :sort,
|
10
|
+
:find_by_id, :find_by_ids, :find_by_id!, :find_by_ids!,
|
11
|
+
:page, :all, :each, :collect, :map,
|
12
|
+
:select, :find_all, :reject, :destroy_all,
|
13
|
+
:ids, :count, :empty?, :exists?,
|
14
|
+
:associated_ids_for
|
15
|
+
|
16
|
+
def initialize(type, parent_klass, parent_id, name)
|
17
|
+
@type = type
|
18
|
+
@parent_klass = parent_klass
|
19
|
+
@parent_id = parent_id
|
20
|
+
@name = name
|
21
|
+
|
22
|
+
@backend = parent_klass.send(:backend)
|
23
|
+
|
24
|
+
@key_type = case @type
|
25
|
+
when :has_many, :has_and_belongs_to_many
|
26
|
+
:set
|
27
|
+
when :has_sorted_set
|
28
|
+
:sorted_set
|
29
|
+
end
|
30
|
+
|
31
|
+
@record_ids_key = Zermelo::Records::Key.new(
|
32
|
+
:klass => parent_klass,
|
33
|
+
:id => parent_id,
|
34
|
+
:name => "#{name}_ids",
|
35
|
+
:type => @key_type,
|
36
|
+
:object => :association
|
37
|
+
)
|
38
|
+
|
39
|
+
parent_klass.send(:with_association_data, name.to_sym) do |data|
|
40
|
+
@associated_class = data.data_klass
|
41
|
+
@lock_klasses = [data.data_klass] + data.related_klasses
|
42
|
+
@inverse = data.inverse
|
43
|
+
@sort_key = data.sort_key
|
44
|
+
@sort_order = data.sort_order
|
45
|
+
@callbacks = data.callbacks
|
46
|
+
end
|
47
|
+
|
48
|
+
raise ':inverse_of must be set' if @inverse.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def first
|
52
|
+
# FIXME raise error unless :has_sorted_set.eql?(@type)
|
53
|
+
filter.first
|
54
|
+
end
|
55
|
+
|
56
|
+
def last
|
57
|
+
# FIXME raise error unless :has_sorted_set.eql?(@type)
|
58
|
+
filter.last
|
59
|
+
end
|
60
|
+
|
61
|
+
def <<(record)
|
62
|
+
add(record)
|
63
|
+
self # for << 'a' << 'b'
|
64
|
+
end
|
65
|
+
|
66
|
+
def add(*records)
|
67
|
+
raise 'No records to add' if records.empty?
|
68
|
+
raise 'Invalid record class' if records.any? {|r| !r.is_a?(@associated_class)}
|
69
|
+
raise 'Record(s) must have been saved' unless records.all? {|r| r.persisted?} # may need to be moved
|
70
|
+
@parent_klass.lock(*@lock_klasses) do
|
71
|
+
record_ids = case @type
|
72
|
+
when :has_many, :has_and_belongs_to_many
|
73
|
+
records.is_a?(Zermelo::Filter) ? records.ids : records.map(&:id)
|
74
|
+
when :has_sorted_set
|
75
|
+
records.map {|r| [r.send(@sort_key.to_sym).to_f, r.id]}
|
76
|
+
end
|
77
|
+
_add_ids({:callbacks => true}, *record_ids)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_ids(*record_ids)
|
82
|
+
raise 'No record ids to add' if record_ids.empty?
|
83
|
+
@parent_klass.lock(*@lock_klasses) do
|
84
|
+
records = @associated_class.find_by_ids!(*record_ids)
|
85
|
+
_add_ids({:callbacks => true}, *record_ids)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# TODO support dependent delete, for now just removes the association
|
90
|
+
def remove(*records)
|
91
|
+
raise 'No records to remove' if records.empty?
|
92
|
+
raise 'Invalid record class' if records.any? {|r| !r.is_a?(@associated_class)}
|
93
|
+
raise 'Record(s) must have been saved' unless records.all? {|r| r.persisted?} # may need to be moved
|
94
|
+
@parent_klass.lock(*@lock_klasses) do
|
95
|
+
_remove_ids({:callbacks => true},
|
96
|
+
*(records.is_a?(Zermelo::Filter) ? records.ids : records.map(&:id)))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def remove_ids(*record_ids)
|
101
|
+
raise 'No record ids to remove' if record_ids.empty?
|
102
|
+
@parent_klass.lock(*@lock_klasses) do
|
103
|
+
_remove_ids({:callbacks => true}, *record_ids)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def clear
|
108
|
+
@parent_klass.lock(*@lock_klasses) do
|
109
|
+
_remove_ids({:callbacks => true}, *filter.ids) unless filter.empty?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def _inverse
|
116
|
+
return @inverse_obj unless @inverse_obj.nil?
|
117
|
+
@associated_class.send(:with_association_data, @inverse.to_sym) do |data|
|
118
|
+
@inverse_obj = case @type
|
119
|
+
when :has_many, :has_sorted_set
|
120
|
+
# inverse is belongs_to
|
121
|
+
# FIXME neater to do multiple hash keys at once, if backends support it
|
122
|
+
Zermelo::Records::Key.new(
|
123
|
+
:klass => @associated_class,
|
124
|
+
:name => 'belongs_to',
|
125
|
+
:type => :hash,
|
126
|
+
:object => :association
|
127
|
+
)
|
128
|
+
when :has_and_belongs_to_many
|
129
|
+
Zermelo::Records::Key.new(
|
130
|
+
:klass => @associated_class,
|
131
|
+
:name => "#{@inverse}_ids",
|
132
|
+
:type => :set,
|
133
|
+
:object => :association
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
@inverse_obj
|
138
|
+
end
|
139
|
+
|
140
|
+
# associated will be a belongs_to; on remove already runs inside a lock and transaction
|
141
|
+
def on_remove
|
142
|
+
_remove_ids({:callbacks => false}, *filter.ids) unless filter.empty?
|
143
|
+
end
|
144
|
+
|
145
|
+
def _add_ids(opts = {}, *record_ids)
|
146
|
+
ba = @callbacks[:before_add]
|
147
|
+
if ba.nil? || !opts[:callbacks] || !@parent_klass.respond_to?(ba) ||
|
148
|
+
!@parent_klass.send(ba, @parent_id, *record_ids).is_a?(FalseClass)
|
149
|
+
|
150
|
+
new_txn = @backend.begin_transaction
|
151
|
+
|
152
|
+
# FIXME neater to do multiple hash keys at once for inverse, if backends support it
|
153
|
+
case @type
|
154
|
+
when :has_many
|
155
|
+
# inverse is belongs_to
|
156
|
+
record_ids.each do |record_id|
|
157
|
+
_inverse.id = record_id
|
158
|
+
@backend.add(_inverse, "#{@inverse}_id" => @parent_id)
|
159
|
+
end
|
160
|
+
when :has_sorted_set
|
161
|
+
# inverse is belongs_to
|
162
|
+
record_ids.each do |(score, record_id)|
|
163
|
+
_inverse.id = record_id
|
164
|
+
@backend.add(_inverse, "#{@inverse}_id" => @parent_id)
|
165
|
+
end
|
166
|
+
when :has_and_belongs_to_many
|
167
|
+
# inverse is has_and_belongs_to_many
|
168
|
+
record_ids.each do |record_id|
|
169
|
+
_inverse.id = record_id
|
170
|
+
@backend.add(_inverse, @parent_id)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
@backend.add(@record_ids_key, record_ids)
|
175
|
+
|
176
|
+
@backend.commit_transaction if new_txn
|
177
|
+
aa = @callbacks[:after_add]
|
178
|
+
if !aa.nil? && opts[:callbacks] && @parent_klass.respond_to?(aa)
|
179
|
+
@parent_klass.send(aa, @parent_id, *record_ids)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def _remove_ids(opts = {}, *record_ids)
|
185
|
+
br = @callbacks[:before_remove]
|
186
|
+
if br.nil? || !opts[:callbacks] || !@parent_klass.respond_to?(br) ||
|
187
|
+
!@parent_klass.send(br, @parent_id, *record_ids).is_a?(FalseClass)
|
188
|
+
|
189
|
+
new_txn = @backend.begin_transaction
|
190
|
+
|
191
|
+
# FIXME neater to do multiple hash keys at once for inverse, if backends support it
|
192
|
+
case @type
|
193
|
+
when :has_many, :has_sorted_set
|
194
|
+
# inverse is belongs_to
|
195
|
+
record_ids.each do |record_id|
|
196
|
+
_inverse.id = record_id
|
197
|
+
@backend.delete(_inverse, "#{@inverse}_id")
|
198
|
+
end
|
199
|
+
when :has_and_belongs_to_many
|
200
|
+
# inverse is has_and_belongs_to_many
|
201
|
+
record_ids.each do |record_id|
|
202
|
+
_inverse.id = record_id
|
203
|
+
@backend.delete(_inverse, @parent_id)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
@backend.delete(@record_ids_key, record_ids)
|
208
|
+
|
209
|
+
@backend.commit_transaction if new_txn
|
210
|
+
|
211
|
+
ar = @callbacks[:after_remove]
|
212
|
+
if !ar.nil? && opts[:callbacks] && @parent_klass.respond_to?(ar)
|
213
|
+
@parent_klass.send(ar, @parent_id, *record_ids)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# creates a new filter class each time it's called, to store the
|
219
|
+
# state for this particular filter chain
|
220
|
+
def filter
|
221
|
+
@backend.filter(@record_ids_key, @associated_class, @parent_klass,
|
222
|
+
@parent_id, @callbacks, @sort_order)
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.associated_ids_for(backend, type, klass, name, *these_ids)
|
226
|
+
key_type = case type
|
227
|
+
when :has_many, :has_and_belongs_to_many
|
228
|
+
:set
|
229
|
+
when :has_sorted_set
|
230
|
+
:sorted_set
|
231
|
+
end
|
232
|
+
|
233
|
+
these_ids.each_with_object({}) do |this_id, memo|
|
234
|
+
key = Zermelo::Records::Key.new(
|
235
|
+
:klass => klass,
|
236
|
+
:id => this_id,
|
237
|
+
:name => "#{name}_ids",
|
238
|
+
:type => key_type,
|
239
|
+
:object => :association
|
240
|
+
)
|
241
|
+
memo[this_id] = backend.get(key)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# NB index instances are all internal to zermelo, not user-accessible
|
2
|
+
|
3
|
+
require 'zermelo/records/key'
|
4
|
+
|
5
|
+
module Zermelo
|
6
|
+
module Associations
|
7
|
+
class RangeIndex
|
8
|
+
|
9
|
+
def initialize(parent_klass, name)
|
10
|
+
@parent_klass = parent_klass
|
11
|
+
@attribute_name = name
|
12
|
+
|
13
|
+
@backend = parent_klass.send(:backend)
|
14
|
+
|
15
|
+
parent_klass.send(:with_index_data, name.to_sym) do |data|
|
16
|
+
@attribute_type = data.type
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete_id(id, value)
|
21
|
+
@backend.delete(key, id)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_id(id, value)
|
25
|
+
@backend.add(key, [@backend.safe_value(@attribute_type, value), id])
|
26
|
+
end
|
27
|
+
|
28
|
+
def move_id(id, value_from, indexer_to, value_to)
|
29
|
+
@backend.move(key, [@backend.safe_value(@attribute_type, value_from), id],
|
30
|
+
indexer_to.key, [@backend.safe_value(@attribute_type, value_to), id])
|
31
|
+
end
|
32
|
+
|
33
|
+
def key
|
34
|
+
@indexer ||= Zermelo::Records::Key.new(
|
35
|
+
:klass => @parent_klass,
|
36
|
+
:name => "by_#{@attribute_name}",
|
37
|
+
:type => :sorted_set,
|
38
|
+
:object => :index
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
module Zermelo
|
2
|
+
module Associations
|
3
|
+
class Singular
|
4
|
+
|
5
|
+
def initialize(type, parent_klass, parent_id, name)
|
6
|
+
@type = type
|
7
|
+
@parent_klass = parent_klass
|
8
|
+
@parent_id = parent_id
|
9
|
+
@name = name
|
10
|
+
|
11
|
+
@backend = parent_klass.send(:backend)
|
12
|
+
|
13
|
+
@record_id_key = Zermelo::Records::Key.new(
|
14
|
+
:klass => parent_klass,
|
15
|
+
:id => parent_id,
|
16
|
+
:name => type.to_s,
|
17
|
+
:type => :hash,
|
18
|
+
:object => :association
|
19
|
+
)
|
20
|
+
|
21
|
+
parent_klass.send(:with_association_data, name.to_sym) do |data|
|
22
|
+
@associated_class = data.data_klass
|
23
|
+
@lock_klasses = [data.data_klass] + data.related_klasses
|
24
|
+
@inverse = data.inverse
|
25
|
+
@callbacks = data.callbacks
|
26
|
+
end
|
27
|
+
|
28
|
+
raise ':inverse_of must be set' if @inverse.nil?
|
29
|
+
@inverse_key = "#{@inverse}_id"
|
30
|
+
end
|
31
|
+
|
32
|
+
def value=(record)
|
33
|
+
if record.nil?
|
34
|
+
@parent_klass.lock(*@lock_klasses) do
|
35
|
+
_clear(:callbacks => true)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise 'Invalid record class' unless record.is_a?(@associated_class)
|
39
|
+
raise 'Record must have been saved' unless record.persisted?
|
40
|
+
@parent_klass.lock(*@lock_klasses) do
|
41
|
+
opts = {:callbacks => true}
|
42
|
+
if :sorted_set.eql?(_inverse.type)
|
43
|
+
opts[:score] = @parent_klass.find_by_id!(@parent_id).send(@inverse_sort_key.to_sym).to_f
|
44
|
+
end
|
45
|
+
_set(opts, record.id)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def value
|
51
|
+
v = nil
|
52
|
+
@parent_klass.lock(*@lock_klasses) do
|
53
|
+
br = @callbacks[:before_read]
|
54
|
+
@parent_klass.send(br, @parent_id) if !br.nil? && @parent_klass.respond_to?(br)
|
55
|
+
id = @backend.get(@record_id_key)["#{@name}_id"]
|
56
|
+
# # TODO maybe: uses hgetall, need separate getter for hash/list/set
|
57
|
+
# @backend.get_hash_value(@record_id_key, "#{@name}_id")
|
58
|
+
v = @associated_class.send(:load, id) unless id.nil?
|
59
|
+
ar = @callbacks[:after_read]
|
60
|
+
@parent_klass.send(ar, @parent_id, v) if !ar.nil? && @parent_klass.respond_to?(ar)
|
61
|
+
end
|
62
|
+
v
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# on_remove already runs inside a lock & transaction
|
68
|
+
def on_remove
|
69
|
+
_clear(:callbacks => false)
|
70
|
+
end
|
71
|
+
|
72
|
+
def _inverse
|
73
|
+
return @inverse_obj unless @inverse_obj.nil?
|
74
|
+
@associated_class.send(:with_association_data, @inverse.to_sym) do |data|
|
75
|
+
@inverse_obj = case @type
|
76
|
+
when :belongs_to
|
77
|
+
key_name, key_type = case data.data_type
|
78
|
+
when :has_many
|
79
|
+
["#{@inverse}_ids", :set]
|
80
|
+
when :has_sorted_set
|
81
|
+
["#{@inverse}_ids", :sorted_set]
|
82
|
+
when :has_one
|
83
|
+
["has_one", :hash]
|
84
|
+
end
|
85
|
+
|
86
|
+
@inverse_sort_key = data.sort_key
|
87
|
+
|
88
|
+
Zermelo::Records::Key.new(
|
89
|
+
:klass => @associated_class,
|
90
|
+
:name => key_name,
|
91
|
+
:type => key_type,
|
92
|
+
:object => :association
|
93
|
+
)
|
94
|
+
when :has_one
|
95
|
+
# inverse is belongs_to
|
96
|
+
Zermelo::Records::Key.new(
|
97
|
+
:klass => @associated_class,
|
98
|
+
:name => 'belongs_to',
|
99
|
+
:type => :hash,
|
100
|
+
:object => :association
|
101
|
+
)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
@inverse_obj
|
105
|
+
end
|
106
|
+
|
107
|
+
def _clear(opts = {})
|
108
|
+
bc = @callbacks[:before_clear]
|
109
|
+
if bc.nil? || !opts[:callbacks] || !@parent_klass.respond_to?(bc) ||
|
110
|
+
!@parent_klass.send(bc, @parent_id).is_a?(FalseClass)
|
111
|
+
|
112
|
+
record_id = @backend.get(@record_id_key)["#{@name}_id"]
|
113
|
+
_inverse.id = record_id
|
114
|
+
|
115
|
+
new_txn = @backend.begin_transaction
|
116
|
+
|
117
|
+
case @type
|
118
|
+
when :belongs_to, :has_one
|
119
|
+
# FIXME can we access the assoc type instead?
|
120
|
+
case _inverse.type
|
121
|
+
when :set, :sorted_set
|
122
|
+
@backend.delete(_inverse, @parent_id)
|
123
|
+
when :hash
|
124
|
+
@backend.delete(_inverse, "#{@inverse}_id")
|
125
|
+
end
|
126
|
+
|
127
|
+
@backend.delete(@record_id_key, "#{@name}_id")
|
128
|
+
end
|
129
|
+
|
130
|
+
@backend.commit_transaction if new_txn
|
131
|
+
|
132
|
+
ac = @callbacks[:after_clear]
|
133
|
+
if !ac.nil? && opts[:callbacks] && @parent_klass.respond_to?(ac)
|
134
|
+
@parent_klass.send(ac, @parent_id, record_id)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def _set(opts = {}, record_id)
|
140
|
+
bs = @callbacks[:before_set]
|
141
|
+
if bs.nil? || !opts[:callbacks] || !@parent_klass.respond_to?(bs) ||
|
142
|
+
!@parent_klass.send(bs, @parent_id, record_id).is_a?(FalseClass)
|
143
|
+
|
144
|
+
_inverse.id = record_id
|
145
|
+
|
146
|
+
new_txn = @backend.begin_transaction
|
147
|
+
|
148
|
+
# FIXME can we access the assoc type instead?
|
149
|
+
case _inverse.type
|
150
|
+
when :set
|
151
|
+
@backend.add(_inverse, @parent_id)
|
152
|
+
when :sorted_set
|
153
|
+
@backend.add(_inverse, [opts[:score], @parent_id])
|
154
|
+
when :hash
|
155
|
+
@backend.add(_inverse, @inverse_key => @parent_id)
|
156
|
+
end
|
157
|
+
|
158
|
+
@backend.add(@record_id_key, "#{@name}_id" => record_id)
|
159
|
+
|
160
|
+
@backend.commit_transaction if new_txn
|
161
|
+
|
162
|
+
as = @callbacks[:after_set]
|
163
|
+
if !as.nil? && opts[:callbacks] && @parent_klass.respond_to?(as)
|
164
|
+
@parent_klass.send(as, @parent_id, record_id)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.associated_ids_for(backend, type, klass, name, inversed, *these_ids)
|
170
|
+
these_ids.each_with_object({}) do |this_id, memo|
|
171
|
+
key = Zermelo::Records::Key.new(
|
172
|
+
:klass => klass,
|
173
|
+
:id => this_id,
|
174
|
+
:name => type.to_s,
|
175
|
+
:type => :hash,
|
176
|
+
:object => :association
|
177
|
+
)
|
178
|
+
|
179
|
+
assoc_id = backend.get(key)["#{name}_id"]
|
180
|
+
# assoc_id = backend.get_hash_value(key, "#{name}_id")
|
181
|
+
|
182
|
+
if inversed
|
183
|
+
memo[assoc_id] ||= []
|
184
|
+
memo[assoc_id] << this_id
|
185
|
+
else
|
186
|
+
memo[this_id] = assoc_id
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# NB index instances are all internal to zermelo, not user-accessible
|
2
2
|
|
3
|
+
require 'zermelo/records/key'
|
4
|
+
|
3
5
|
module Zermelo
|
4
6
|
module Associations
|
5
7
|
class UniqueIndex
|
@@ -10,8 +12,6 @@ module Zermelo
|
|
10
12
|
|
11
13
|
@backend = parent_klass.send(:backend)
|
12
14
|
|
13
|
-
@indexers = {}
|
14
|
-
|
15
15
|
parent_klass.send(:with_index_data, name.to_sym) do |data|
|
16
16
|
@attribute_type = data.type
|
17
17
|
end
|
@@ -26,7 +26,8 @@ module Zermelo
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def move_id(id, value_from, indexer_to, value_to)
|
29
|
-
@backend.move(key, {@backend.index_keys(@attribute_type,
|
29
|
+
@backend.move(key, {@backend.index_keys(@attribute_type, value_from).join(':') => id},
|
30
|
+
indexer_to.key, {@backend.index_keys(@attribute_type, value_to).join(':') => id})
|
30
31
|
end
|
31
32
|
|
32
33
|
def key
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
require 'zermelo/locks/no_lock'
|
4
|
+
|
5
|
+
module Zermelo
|
6
|
+
|
7
|
+
module Backend
|
8
|
+
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
def escape_key_name(name)
|
12
|
+
name.gsub(/%/, '%%').gsub(/ /, '%20').gsub(/:/, '%3A')
|
13
|
+
end
|
14
|
+
|
15
|
+
def unescape_key_name(name)
|
16
|
+
name.gsub(/%3A/, ':').gsub(/%20/, ' ').gsub(/%%/, '%')
|
17
|
+
end
|
18
|
+
|
19
|
+
def safe_value(type, value)
|
20
|
+
case type
|
21
|
+
when :string, :integer
|
22
|
+
value.to_s
|
23
|
+
when :float, :timestamp
|
24
|
+
value.to_f
|
25
|
+
when :boolean
|
26
|
+
(!!value).to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def index_keys(type, value)
|
31
|
+
return ["null", "null"] if value.nil?
|
32
|
+
|
33
|
+
case type
|
34
|
+
when :string
|
35
|
+
["string", escape_key_name(value)]
|
36
|
+
when :integer
|
37
|
+
["integer", escape_key_name(value.to_s)]
|
38
|
+
when :float
|
39
|
+
["float", escape_key_name(value.to_s)]
|
40
|
+
when :timestamp
|
41
|
+
case value
|
42
|
+
when Integer
|
43
|
+
["timestamp", escape_key_name(value.to_s)]
|
44
|
+
when Time, DateTime
|
45
|
+
["timestamp", escape_key_name(value.to_f.to_s)]
|
46
|
+
end
|
47
|
+
when :boolean
|
48
|
+
case value
|
49
|
+
when TrueClass
|
50
|
+
["boolean", "true"]
|
51
|
+
when FalseClass
|
52
|
+
["boolean", "false"]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# for hashes, lists, sets
|
58
|
+
def add(key, value)
|
59
|
+
change(:add, key, value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def delete(key, value)
|
63
|
+
change(:delete, key, value)
|
64
|
+
end
|
65
|
+
|
66
|
+
def move(key_from, value_from, key_to, value_to)
|
67
|
+
change(:move, key_from, value_from, key_to, value_to)
|
68
|
+
end
|
69
|
+
|
70
|
+
def clear(key)
|
71
|
+
change(:clear, key)
|
72
|
+
end
|
73
|
+
|
74
|
+
# works for both simple and complex types (i.e. strings, numbers, booleans,
|
75
|
+
# hashes, lists, sets)
|
76
|
+
def set(key, value)
|
77
|
+
change(:set, key, value)
|
78
|
+
end
|
79
|
+
|
80
|
+
def purge(key)
|
81
|
+
change(:purge, key)
|
82
|
+
end
|
83
|
+
|
84
|
+
def get(attr_key)
|
85
|
+
get_multiple(attr_key)[attr_key.klass.send(:class_key)][attr_key.id][attr_key.name.to_s]
|
86
|
+
end
|
87
|
+
|
88
|
+
def lock(*klasses, &block)
|
89
|
+
ret = nil
|
90
|
+
# doesn't handle re-entrant case for influxdb, which has no locking yet
|
91
|
+
locking = Thread.current[:zermelo_locking]
|
92
|
+
if locking.nil?
|
93
|
+
lock_proc = proc do
|
94
|
+
begin
|
95
|
+
Thread.current[:zermelo_locking] = klasses
|
96
|
+
ret = block.call
|
97
|
+
ensure
|
98
|
+
Thread.current[:zermelo_locking] = nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
lock_klass = case self
|
103
|
+
when Zermelo::Backends::Redis
|
104
|
+
Zermelo::Locks::RedisLock
|
105
|
+
else
|
106
|
+
Zermelo::Locks::NoLock
|
107
|
+
end
|
108
|
+
|
109
|
+
lock_klass.new.lock(*klasses, &lock_proc)
|
110
|
+
else
|
111
|
+
# accepts any subset of 'locking'
|
112
|
+
unless (klasses - locking).empty?
|
113
|
+
raise "Currently locking #{locking.map(&:name)}, cannot lock different set #{klasses.map(&:name)}"
|
114
|
+
end
|
115
|
+
ret = block.call
|
116
|
+
end
|
117
|
+
ret
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|