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,115 @@
1
+ # The other side of a has_one, has_many, or has_sorted_set association
2
+
3
+ module Zermelo
4
+ module Associations
5
+ class BelongsTo
6
+
7
+ # NB a single instance of this class doesn't need to care about the hash
8
+ # used for storage, that should be done in the save method of the parent
9
+
10
+ def initialize(parent, name)
11
+ @parent = parent
12
+ @name = name
13
+
14
+ @backend = parent.send(:backend)
15
+
16
+ @record_ids_key = Zermelo::Records::Key.new(
17
+ :klass => parent.class.send(:class_key),
18
+ :id => parent.id,
19
+ :name => 'belongs_to',
20
+ :type => :hash,
21
+ :object => :association
22
+ )
23
+
24
+ parent.class.send(:with_association_data, name.to_sym) do |data|
25
+ @associated_class = data.data_klass
26
+ @lock_klasses = [data.data_klass] + data.related_klasses
27
+ @inverse = data.inverse
28
+ @callbacks = data.callbacks
29
+ end
30
+
31
+ raise ':inverse_of must be set' if @inverse.nil?
32
+ @inverse_key = "#{name}_id"
33
+ end
34
+
35
+ def value=(record)
36
+ if record.nil?
37
+ @parent.class.lock(*@lock_klasses) do
38
+ r = @associated_class.send(:load, @backend.get(@record_ids_key)[@inverse_key.to_s])
39
+ bc = @callbacks[:before_clear]
40
+ if bc.nil? || !@parent.respond_to?(bc) || !@parent.send(bc, r).is_a?(FalseClass)
41
+ new_txn = @backend.begin_transaction
42
+ @backend.delete(@record_ids_key, @inverse_key)
43
+ @backend.commit_transaction if new_txn
44
+ ac = @callbacks[:after_clear]
45
+ @parent.send(ac, r) if !ac.nil? && @parent.respond_to?(ac)
46
+ end
47
+ end
48
+ else
49
+ raise 'Invalid record class' unless record.is_a?(@associated_class)
50
+ raise 'Record must have been saved' unless record.persisted?
51
+ @parent.class.lock(*@lock_klasses) do
52
+ bs = @callbacks[:before_set]
53
+ if bs.nil? || !@parent.respond_to?(bs) || !@parent.send(bs, r).is_a?(FalseClass)
54
+ new_txn = @backend.begin_transaction
55
+ @backend.add(@record_ids_key, @inverse_key => record.id)
56
+ @backend.commit_transaction if new_txn
57
+ as = @callbacks[:after_set]
58
+ @parent.send(as, record) if !as.nil? && @parent.respond_to?(as)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def value
65
+ @parent.class.lock(*@lock_klasses) do
66
+ # FIXME uses hgetall, need separate getter for hash/list/set
67
+ if id = @backend.get(@record_ids_key)[@inverse_key.to_s]
68
+ # if id = @backend.get_hash_value(@record_ids_key, @inverse_key.to_s)
69
+ @associated_class.send(:load, id)
70
+ else
71
+ nil
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ # on_remove already runs inside a lock & transaction
79
+ def on_remove
80
+ unless value.nil?
81
+ assoc = value.send("#{@inverse}_proxy".to_sym)
82
+ if assoc.respond_to?(:delete)
83
+ assoc.send(:delete, @parent)
84
+ elsif assoc.respond_to?(:value=)
85
+ assoc.send(:value=, nil)
86
+ end
87
+ end
88
+ @backend.clear(@record_ids_key)
89
+ end
90
+
91
+ def self.associated_ids_for(backend, class_key, name, inversed, *these_ids)
92
+ these_ids.each_with_object({}) do |this_id, memo|
93
+ key = Zermelo::Records::Key.new(
94
+ :klass => class_key,
95
+ :id => this_id,
96
+ :name => 'belongs_to',
97
+ :type => :hash,
98
+ :object => :association
99
+ )
100
+
101
+ assoc_id = backend.get(key)["#{name}_id"]
102
+ # assoc_id = backend.get_hash_value(key, "#{name}_id")
103
+
104
+ if inversed
105
+ memo[assoc_id] ||= []
106
+ memo[assoc_id] << this_id
107
+ else
108
+ memo[this_id] = assoc_id
109
+ end
110
+ end
111
+ end
112
+
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,244 @@
1
+ require 'zermelo/associations/association_data'
2
+ require 'zermelo/associations/index_data'
3
+
4
+ require 'zermelo/associations/belongs_to'
5
+ require 'zermelo/associations/has_and_belongs_to_many'
6
+ require 'zermelo/associations/has_many'
7
+ require 'zermelo/associations/has_one'
8
+ require 'zermelo/associations/has_sorted_set'
9
+ require 'zermelo/associations/index'
10
+ require 'zermelo/associations/unique_index'
11
+
12
+ # NB: this module gets mixed in to Zermelo::Record as class methods
13
+
14
+ # TODO update other side of associations without having to load the record (?)
15
+ # TODO callbacks on before/after add/delete on association?
16
+
17
+ module Zermelo
18
+ module Associations
19
+ module ClassMethods
20
+
21
+ protected
22
+
23
+ # used by classes including a Zermelo Record to set up
24
+ # indices and associations
25
+ def index_by(*args)
26
+ att_types = attribute_types
27
+ args.each do |arg|
28
+ index(::Zermelo::Associations::Index, arg, :type => att_types[arg])
29
+ end
30
+ nil
31
+ end
32
+
33
+ def unique_index_by(*args)
34
+ att_types = attribute_types
35
+ args.each do |arg|
36
+ index(::Zermelo::Associations::UniqueIndex, arg, :type => att_types[arg])
37
+ end
38
+ nil
39
+ end
40
+
41
+ def has_many(name, args = {})
42
+ associate(::Zermelo::Associations::HasMany, name, args)
43
+ nil
44
+ end
45
+
46
+ def has_one(name, args = {})
47
+ associate(::Zermelo::Associations::HasOne, name, args)
48
+ nil
49
+ end
50
+
51
+ def has_sorted_set(name, args = {})
52
+ associate(::Zermelo::Associations::HasSortedSet, name, args)
53
+ nil
54
+ end
55
+
56
+ def has_and_belongs_to_many(name, args = {})
57
+ associate(::Zermelo::Associations::HasAndBelongsToMany, name, args)
58
+ nil
59
+ end
60
+
61
+ def belongs_to(name, args = {})
62
+ associate(::Zermelo::Associations::BelongsTo, name, args)
63
+ nil
64
+ end
65
+ # end used by client classes
66
+
67
+ # used internally by other parts of Zermelo to implement the above
68
+ # configuration
69
+
70
+ # Works out which classes should be locked when updating associations
71
+ # TODO work out if this can be replaced by 'related_klasses' assoc data
72
+ def associated_classes(visited = [], cascade = true)
73
+ visited |= [self]
74
+ return visited unless cascade
75
+ @lock.synchronize do
76
+ @association_data ||= {}
77
+ @association_data.values.each do |data|
78
+ klass = data.data_klass
79
+ next if visited.include?(klass)
80
+ visited |= klass.associated_classes(visited, false)
81
+ end
82
+ end
83
+ visited
84
+ end
85
+
86
+ # TODO for each association: check whether it has changed
87
+ # would need an instance-level hash with association name as key,
88
+ # boolean 'changed' value
89
+ def with_associations(record)
90
+ @lock.synchronize do
91
+ @association_data ||= {}
92
+ @association_data.keys.each do |name|
93
+ yield record.send("#{name}_proxy".to_sym)
94
+ end
95
+ end
96
+ end
97
+
98
+ def with_association_data(name = nil)
99
+ @lock.synchronize do
100
+ @association_data ||= {}
101
+ assoc_data = name.nil? ? @association_data : @association_data[name]
102
+ yield assoc_data unless assoc_data.nil?
103
+ end
104
+ end
105
+
106
+ def with_index_data(name = nil)
107
+ @lock.synchronize do
108
+ @index_data ||= {}
109
+ idx_data = name.nil? ? @index_data : @index_data[name]
110
+ yield idx_data unless idx_data.nil?
111
+ end
112
+ end
113
+ # end used internally within Zermelo
114
+
115
+ # # TODO can remove need for some of the inverse mapping
116
+ # # was inverse_of(source, klass)
117
+ # with_association_data do |d|
118
+ # d.detect {|name, data| data.klass == klass && data.inverse == source}
119
+ # end
120
+
121
+ private
122
+
123
+ def add_index_data(klass, name, args = {})
124
+ return if name.nil?
125
+
126
+ data = Zermelo::Associations::IndexData.new(
127
+ :name => name,
128
+ :type => args[:type],
129
+ :index_klass => klass
130
+ )
131
+
132
+ @lock.synchronize do
133
+ @index_data ||= {}
134
+ @index_data[name] = data
135
+ end
136
+ end
137
+
138
+ def index(klass, name, args = {})
139
+ return if name.nil?
140
+
141
+ add_index_data(klass, name, args)
142
+
143
+ idx = %Q{
144
+ private
145
+
146
+ def #{name}_index
147
+ @#{name}_index ||= #{klass.name}.new(self, '#{name}')
148
+ @#{name}_index
149
+ end
150
+ }
151
+ instance_eval idx, __FILE__, __LINE__
152
+ end
153
+
154
+ def add_association_data(klass, name, args = {})
155
+
156
+ # TODO have inverse be a reference (or copy?) of the association data
157
+ # record for that inverse association; would need to defer lookup until
158
+ # all data in place for all assocs, so might be best if looked up and
159
+ # cached on first use
160
+ inverse = if args[:inverse_of].nil? || args[:inverse_of].to_s.empty?
161
+ nil
162
+ else
163
+ args[:inverse_of].to_s
164
+ end
165
+
166
+ callbacks = case klass.name
167
+ when ::Zermelo::Associations::HasMany.name,
168
+ ::Zermelo::Associations::HasSortedSet.name,
169
+ ::Zermelo::Associations::HasAndBelongsToMany.name
170
+ [:before_add, :after_add, :before_remove, :after_remove]
171
+ when ::Zermelo::Associations::HasOne.name,
172
+ ::Zermelo::Associations::BelongsTo.name
173
+ [:before_set, :after_set, :before_clear, :after_clear]
174
+ else
175
+ []
176
+ end
177
+
178
+ data = Zermelo::Associations::AssociationData.new(
179
+ :name => name,
180
+ :data_klass_name => args[:class_name],
181
+ :type_klass => klass,
182
+ :inverse => inverse,
183
+ :related_klass_names => args[:related_class_names],
184
+ :callbacks => callbacks.each_with_object({}) {|c, memo|
185
+ memo[c] = args[c]
186
+ }
187
+ )
188
+
189
+ if klass.name == Zermelo::Associations::HasSortedSet.name
190
+ data.sort_key = (args[:key] || :id)
191
+ end
192
+
193
+ @lock.synchronize do
194
+ @association_data ||= {}
195
+ @association_data[name] = data
196
+ end
197
+ end
198
+
199
+ def associate(klass, name, args = {})
200
+ return if name.nil?
201
+
202
+ add_association_data(klass, name, args)
203
+
204
+ assoc = case klass.name
205
+ when ::Zermelo::Associations::HasMany.name,
206
+ ::Zermelo::Associations::HasSortedSet.name,
207
+ ::Zermelo::Associations::HasAndBelongsToMany.name
208
+
209
+ %Q{
210
+ def #{name}
211
+ #{name}_proxy
212
+ end
213
+ }
214
+
215
+ when ::Zermelo::Associations::HasOne.name,
216
+ ::Zermelo::Associations::BelongsTo.name
217
+
218
+ %Q{
219
+ def #{name}
220
+ #{name}_proxy.value
221
+ end
222
+
223
+ def #{name}=(obj)
224
+ #{name}_proxy.value = obj
225
+ end
226
+ }
227
+ end
228
+
229
+ return if assoc.nil?
230
+
231
+ proxy = %Q{
232
+ def #{name}_proxy
233
+ raise "Associations cannot be invoked for records without an id" if self.id.nil?
234
+
235
+ @#{name}_proxy ||= #{klass.name}.new(self, '#{name}')
236
+ end
237
+ private :#{name}_proxy
238
+ }
239
+ class_eval proxy, __FILE__, __LINE__
240
+ class_eval assoc, __FILE__, __LINE__
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,128 @@
1
+ require 'forwardable'
2
+
3
+ # much like a has_many, but with different add/remove behaviour, as it's paired
4
+ # with another has_and_belongs_to_many association. both sides must set the
5
+ # inverse association name.
6
+
7
+ module Zermelo
8
+ module Associations
9
+ class HasAndBelongsToMany
10
+
11
+ extend Forwardable
12
+
13
+ def_delegators :filter, :intersect, :union, :diff, :sort,
14
+ :find_by_id, :find_by_ids, :find_by_id!, :find_by_ids!,
15
+ :page, :all, :each, :collect, :map,
16
+ :select, :find_all, :reject, :destroy_all,
17
+ :ids, :count, :empty?, :exists?,
18
+ :associated_ids_for
19
+
20
+ def initialize(parent, name)
21
+ @parent = parent
22
+
23
+ @backend = parent.send(:backend)
24
+
25
+ @record_ids_key = Zermelo::Records::Key.new(
26
+ :klass => parent.class.send(:class_key),
27
+ :id => parent.id,
28
+ :name => "#{name}_ids",
29
+ :type => :set,
30
+ :object => :association
31
+ )
32
+
33
+ parent.class.send(:with_association_data, name.to_sym) do |data|
34
+ @associated_class = data.data_klass
35
+ @lock_klasses = [data.data_klass] + data.related_klasses
36
+ @inverse = data.inverse
37
+ @callbacks = data.callbacks
38
+ end
39
+ end
40
+
41
+ def <<(record)
42
+ add(record)
43
+ self # for << 'a' << 'b'
44
+ end
45
+
46
+ def add(*records)
47
+ raise 'No records to add' if records.empty?
48
+ raise 'Invalid record class' unless records.all? {|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
+ records.each do |record|
54
+ @associated_class.send(:load, record.id).send(@inverse.to_sym).
55
+ send(:add_without_inverse, @parent)
56
+ end
57
+ add_without_inverse(*records)
58
+ aa = @callbacks[:after_add]
59
+ @parent.send(aa, *records) if !aa.nil? && @parent.respond_to?(aa)
60
+ end
61
+ end
62
+ end
63
+
64
+ # TODO support dependent delete, for now just deletes the association
65
+ def delete(*records)
66
+ raise 'No records to delete' if records.empty?
67
+ raise 'Invalid record class' unless records.all? {|r| r.is_a?(@associated_class)}
68
+ raise "Record(s) must have been saved" unless records.all? {|r| r.persisted?}
69
+ @parent.class.lock(*@lock_klasses) do
70
+ br = @callbacks[:before_remove]
71
+ if br.nil? || !@parent.respond_to?(br) || !@parent.send(br, *records).is_a?(FalseClass)
72
+ records.each do |record|
73
+ @associated_class.send(:load, record.id).send(@inverse.to_sym).
74
+ send(:delete_without_inverse, @parent)
75
+ end
76
+ delete_without_inverse(*records)
77
+ ar = @callbacks[:after_remove]
78
+ @parent.send(ar, *records) if !ar.nil? && @parent.respond_to?(ar)
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def add_without_inverse(*records)
86
+ new_txn = @backend.begin_transaction
87
+ @backend.add(@record_ids_key, records.map(&:id))
88
+ @backend.commit_transaction if new_txn
89
+ end
90
+
91
+ def delete_without_inverse(*records)
92
+ new_txn = @backend.begin_transaction
93
+ @backend.delete(@record_ids_key, records.map(&:id))
94
+ @backend.commit_transaction if new_txn
95
+ end
96
+
97
+ # associated will be the other side of the HaBTM; on_remove is always
98
+ # called inside a lock
99
+ def on_remove
100
+ ids.each do |record_id|
101
+ @associated_class.send(:load, record_id).send(@inverse.to_sym).
102
+ send(:delete_without_inverse, @parent)
103
+ end
104
+ @backend.purge(@record_ids_key)
105
+ end
106
+
107
+ # creates a new filter class each time it's called, to store the
108
+ # state for this particular filter chain
109
+ def filter
110
+ @backend.filter(@record_ids_key, @associated_class)
111
+ end
112
+
113
+ def self.associated_ids_for(backend, class_key, name, *these_ids)
114
+ these_ids.each_with_object({}) do |this_id, memo|
115
+ key = Zermelo::Records::Key.new(
116
+ :klass => class_key,
117
+ :id => this_id,
118
+ :name => "#{name}_ids",
119
+ :type => :set,
120
+ :object => :association
121
+ )
122
+ memo[this_id] = backend.get(key)
123
+ end
124
+ end
125
+
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,120 @@
1
+ require 'forwardable'
2
+
3
+ module Zermelo
4
+ module Associations
5
+ class HasMany
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(parent, name)
17
+ @parent = parent
18
+
19
+ @backend = parent.send(:backend)
20
+
21
+ @record_ids_key = Zermelo::Records::Key.new(
22
+ :klass => parent.class.send(:class_key),
23
+ :id => parent.id,
24
+ :name => "#{name}_ids",
25
+ :type => :set,
26
+ :object => :association
27
+ )
28
+
29
+ parent.class.send(:with_association_data, name.to_sym) do |data|
30
+ @associated_class = data.data_klass
31
+ @lock_klasses = [data.data_klass] + data.related_klasses
32
+ @inverse = data.inverse
33
+ @callbacks = data.callbacks
34
+ end
35
+ end
36
+
37
+ def <<(record)
38
+ add(record)
39
+ self # for << 'a' << 'b'
40
+ end
41
+
42
+ def add(*records)
43
+ raise 'No records to add' if records.empty?
44
+ raise 'Invalid record class' if records.any? {|r| !r.is_a?(@associated_class)}
45
+ raise 'Record(s) must have been saved' unless records.all? {|r| r.persisted?} # may need to be moved
46
+ @parent.class.lock(*@lock_klasses) do
47
+ ba = @callbacks[:before_add]
48
+ if ba.nil? || !@parent.respond_to?(ba) || !@parent.send(ba, *records).is_a?(FalseClass)
49
+ unless @inverse.nil?
50
+ records.each do |record|
51
+ @associated_class.send(:load, record.id).send("#{@inverse}=", @parent)
52
+ end
53
+ end
54
+
55
+ new_txn = @backend.begin_transaction
56
+ @backend.add(@record_ids_key, records.map(&:id))
57
+ @backend.commit_transaction if new_txn
58
+ aa = @callbacks[:after_add]
59
+ @parent.send(aa, *records) if !aa.nil? && @parent.respond_to?(aa)
60
+ end
61
+ end
62
+ end
63
+
64
+ # TODO support dependent delete, for now just deletes the association
65
+ def delete(*records)
66
+ raise 'No records to delete' if records.empty?
67
+ raise 'Invalid record class' if records.any? {|r| !r.is_a?(@associated_class)}
68
+ raise 'Record(s) must have been saved' unless records.all? {|r| r.persisted?} # may need to be moved
69
+ @parent.class.lock(*@lock_klasses) do
70
+ br = @callbacks[:before_remove]
71
+ if br.nil? || !@parent.respond_to?(br) || !@parent.send(br, *records).is_a?(FalseClass)
72
+ unless @inverse.nil?
73
+ records.each do |record|
74
+ @associated_class.send(:load, record.id).send("#{@inverse}=", nil)
75
+ end
76
+ end
77
+
78
+ new_txn = @backend.begin_transaction
79
+ @backend.delete(@record_ids_key, records.map(&:id))
80
+ @backend.commit_transaction if new_txn
81
+ ar = @callbacks[:after_remove]
82
+ @parent.send(ar, *records) if !ar.nil? && @parent.respond_to?(ar)
83
+ end
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ # associated will be a belongs_to; on remove already runs inside a lock and transaction
90
+ def on_remove
91
+ unless @inverse.nil?
92
+ self.ids.each do |record_id|
93
+ @associated_class.send(:load, record_id).send("#{@inverse}=", nil)
94
+ end
95
+ end
96
+ @backend.clear(@record_ids_key)
97
+ end
98
+
99
+ # creates a new filter class each time it's called, to store the
100
+ # state for this particular filter chain
101
+ def filter
102
+ @backend.filter(@record_ids_key, @associated_class)
103
+ end
104
+
105
+ def self.associated_ids_for(backend, class_key, name, *these_ids)
106
+ these_ids.each_with_object({}) do |this_id, memo|
107
+ key = Zermelo::Records::Key.new(
108
+ :klass => class_key,
109
+ :id => this_id,
110
+ :name => "#{name}_ids",
111
+ :type => :set,
112
+ :object => :association
113
+ )
114
+ memo[this_id] = backend.get(key)
115
+ end
116
+ end
117
+
118
+ end
119
+ end
120
+ end