zermelo 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +76 -52
  4. data/lib/zermelo/associations/association_data.rb +4 -3
  5. data/lib/zermelo/associations/class_methods.rb +37 -50
  6. data/lib/zermelo/associations/index.rb +3 -1
  7. data/lib/zermelo/associations/multiple.rb +247 -0
  8. data/lib/zermelo/associations/range_index.rb +44 -0
  9. data/lib/zermelo/associations/singular.rb +193 -0
  10. data/lib/zermelo/associations/unique_index.rb +4 -3
  11. data/lib/zermelo/backend.rb +120 -0
  12. data/lib/zermelo/backends/{influxdb_backend.rb → influxdb.rb} +87 -31
  13. data/lib/zermelo/backends/{redis_backend.rb → redis.rb} +53 -58
  14. data/lib/zermelo/backends/stub.rb +43 -0
  15. data/lib/zermelo/filter.rb +194 -0
  16. data/lib/zermelo/filters/index_range.rb +22 -0
  17. data/lib/zermelo/filters/{influxdb_filter.rb → influxdb.rb} +12 -11
  18. data/lib/zermelo/filters/redis.rb +173 -0
  19. data/lib/zermelo/filters/steps/list_step.rb +48 -30
  20. data/lib/zermelo/filters/steps/set_step.rb +148 -89
  21. data/lib/zermelo/filters/steps/sort_step.rb +2 -2
  22. data/lib/zermelo/record.rb +53 -0
  23. data/lib/zermelo/records/attributes.rb +32 -0
  24. data/lib/zermelo/records/class_methods.rb +12 -25
  25. data/lib/zermelo/records/{influxdb_record.rb → influxdb.rb} +3 -4
  26. data/lib/zermelo/records/instance_methods.rb +9 -8
  27. data/lib/zermelo/records/key.rb +3 -1
  28. data/lib/zermelo/records/redis.rb +17 -0
  29. data/lib/zermelo/records/stub.rb +17 -0
  30. data/lib/zermelo/version.rb +1 -1
  31. data/spec/lib/zermelo/associations/index_spec.rb +70 -1
  32. data/spec/lib/zermelo/associations/multiple_spec.rb +1084 -0
  33. data/spec/lib/zermelo/associations/range_index_spec.rb +77 -0
  34. data/spec/lib/zermelo/associations/singular_spec.rb +149 -0
  35. data/spec/lib/zermelo/associations/unique_index_spec.rb +58 -2
  36. data/spec/lib/zermelo/filter_spec.rb +363 -0
  37. data/spec/lib/zermelo/locks/redis_lock_spec.rb +3 -3
  38. data/spec/lib/zermelo/records/instance_methods_spec.rb +206 -0
  39. data/spec/spec_helper.rb +9 -1
  40. data/spec/support/mock_logger.rb +48 -0
  41. metadata +31 -46
  42. data/lib/zermelo/associations/belongs_to.rb +0 -115
  43. data/lib/zermelo/associations/has_and_belongs_to_many.rb +0 -128
  44. data/lib/zermelo/associations/has_many.rb +0 -120
  45. data/lib/zermelo/associations/has_one.rb +0 -109
  46. data/lib/zermelo/associations/has_sorted_set.rb +0 -124
  47. data/lib/zermelo/backends/base.rb +0 -115
  48. data/lib/zermelo/filters/base.rb +0 -212
  49. data/lib/zermelo/filters/redis_filter.rb +0 -111
  50. data/lib/zermelo/filters/steps/sorted_set_step.rb +0 -156
  51. data/lib/zermelo/records/base.rb +0 -62
  52. data/lib/zermelo/records/redis_record.rb +0 -27
  53. data/spec/lib/zermelo/associations/belongs_to_spec.rb +0 -6
  54. data/spec/lib/zermelo/associations/has_many_spec.rb +0 -6
  55. data/spec/lib/zermelo/associations/has_one_spec.rb +0 -6
  56. data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +0 -6
  57. data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
  58. data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
  59. data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
  60. data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
  61. data/spec/lib/zermelo/records/influxdb_record_spec.rb +0 -434
  62. data/spec/lib/zermelo/records/key_spec.rb +0 -6
  63. data/spec/lib/zermelo/records/redis_record_spec.rb +0 -1461
  64. data/spec/lib/zermelo/records/type_validator_spec.rb +0 -6
  65. data/spec/lib/zermelo/version_spec.rb +0 -6
  66. data/spec/lib/zermelo_spec.rb +0 -6
@@ -1,6 +1,6 @@
1
- require 'zermelo/backends/base'
1
+ require 'zermelo/backend'
2
2
 
3
- require 'zermelo/filters/influxdb_filter'
3
+ require 'zermelo/filters/influxdb'
4
4
 
5
5
  # NB influxdb doesn't support individually addressable deletes, so
6
6
  # this backend only works to write new records
@@ -11,16 +11,19 @@ module Zermelo
11
11
 
12
12
  module Backends
13
13
 
14
- class InfluxDBBackend
14
+ class InfluxDB
15
15
 
16
- include Zermelo::Backends::Base
16
+ include Zermelo::Backend
17
17
 
18
- def default_sorted_set_key
19
- :time
20
- end
18
+ # def default_sorted_set_key
19
+ # :time
20
+ # end
21
+
22
+ def filter(ids_key, associated_class, callback_target_class = nil,
23
+ callback_target_id = nil, callbacks = nil, sort_order = nil)
21
24
 
22
- def filter(ids_key, record)
23
- Zermelo::Filters::InfluxDBFilter.new(self, ids_key, record)
25
+ Zermelo::Filters::InfluxDB.new(self, ids_key, associated_class,
26
+ callback_target_class, callback_target_id, callbacks, sort_order)
24
27
  end
25
28
 
26
29
  # TODO get filter calling this instead of using same logic
@@ -38,7 +41,7 @@ module Zermelo
38
41
  begin
39
42
  records = Zermelo.influxdb.query("SELECT #{attr_key.name} FROM " +
40
43
  "\"#{class_key}/#{attr_key.id}\" LIMIT 1")["#{class_key}/#{attr_key.id}"]
41
- rescue InfluxDB::Error => ide
44
+ rescue ::InfluxDB::Error => ide
42
45
  raise unless
43
46
  /^Field #{attr_key.name} doesn't exist in series #{class_key}\/#{attr_key.id}$/ === ide.message
44
47
 
@@ -50,9 +53,16 @@ module Zermelo
50
53
  memo[class_key][attr_key.id] ||= {}
51
54
 
52
55
  memo[class_key][attr_key.id][attr_key.name.to_s] = if value.nil?
53
- nil
56
+ case attr_key.type
57
+ when :list
58
+ []
59
+ when :hash
60
+ {}
61
+ when :set
62
+ Set.new()
63
+ else nil
64
+ end
54
65
  else
55
-
56
66
  case attr_key.type
57
67
  when :string
58
68
  value.to_s
@@ -130,11 +140,29 @@ module Zermelo
130
140
  class_key = key.klass.send(:class_key)
131
141
 
132
142
  records[class_key] ||= {}
133
- records[class_key][key.id] ||= {}
134
143
 
135
- records[class_key][key.id][key.name] = case op
144
+ if records[class_key][key.id].nil?
145
+ begin
146
+ result = Zermelo.influxdb.query("SELECT * FROM \"#{class_key}/#{key.id}\" LIMIT 1")["#{class_key}/#{key.id}"]
147
+ if result.nil?
148
+ records[class_key][key.id] = {}
149
+ else
150
+ records[class_key][key.id] = result.first
151
+ records[class_key][key.id].delete_if {|k,v| ["time", "sequence_number"].include?(k) }
152
+ end
153
+ rescue ::InfluxDB::Error => ide
154
+ raise unless
155
+ (/^Couldn't look up columns for series: #{class_key}\/#{key.id}$/ === ide.message) ||
156
+ (/^Couldn't look up columns$/ === ide.message) ||
157
+ (/^Couldn't find series: #{class_key}\/#{key.id}$/ === ide.message)
158
+
159
+ records[class_key][key.id] = {}
160
+ end
161
+ end
162
+
163
+ case op
136
164
  when :set
137
- case key.type
165
+ records[class_key][key.id][key.name] = case key.type
138
166
  when :string, :integer
139
167
  value.nil? ? nil : value.to_s
140
168
  when :timestamp
@@ -148,12 +176,51 @@ module Zermelo
148
176
  end
149
177
  when :add
150
178
  case key.type
151
- when :list, :hash
152
- value
179
+ when :hash
180
+ if records[class_key][key.id][key.name].nil?
181
+ records[class_key][key.id][key.name] = value
182
+ else
183
+ records[class_key][key.id][key.name].update(value)
184
+ end
185
+ when :list
186
+ if records[class_key][key.id][key.name].nil?
187
+ records[class_key][key.id][key.name] = value
188
+ else
189
+ records[class_key][key.id][key.name] += value
190
+ end
153
191
  when :set
154
- value.to_a
192
+ v = value.to_a
193
+ if records[class_key][key.id][key.name].nil?
194
+ records[class_key][key.id][key.name] = v
195
+ else
196
+ records[class_key][key.id][key.name] += v
197
+ end
198
+ when :sorted_set
199
+ v = (1...value.size).step(2).collect {|i| value[i] }
200
+ if records[class_key][key.id][key.name].nil?
201
+ records[class_key][key.id][key.name] = v
202
+ else
203
+ records[class_key][key.id][key.name] += v
204
+ end
205
+ end
206
+ when :delete
207
+ case key.type
208
+ when :hash
209
+ unless records[class_key][key.id][key.name].nil?
210
+ # FIXME
211
+ end
212
+ when :list
213
+ unless records[class_key][key.id][key.name].nil?
214
+ records[class_key][key.id][key.name] -= value
215
+ end
216
+ when :set
217
+ unless records[class_key][key.id][key.name].nil?
218
+ records[class_key][key.id][key.name] -= value.to_a
219
+ end
155
220
  when :sorted_set
156
- (1...value.size).step(2).collect {|i| value[i] }
221
+ unless records[class_key][key.id][key.name].nil?
222
+ records[class_key][key.id][key.name] -= (1...value.size).step(2).collect {|i| value[i] }
223
+ end
157
224
  end
158
225
  when :purge
159
226
  purges << "\"#{class_key}/#{key.id}\""
@@ -163,19 +230,8 @@ module Zermelo
163
230
 
164
231
  records.each_pair do |class_key, klass_records|
165
232
  klass_records.each_pair do |id, data|
166
- begin
167
- prior = Zermelo.influxdb.query("SELECT * FROM \"#{class_key}/#{id}\" LIMIT 1")["#{class_key}/#{id}"]
168
- rescue InfluxDB::Error => ide
169
- raise unless
170
- (/^Couldn't look up columns for series: #{class_key}\/#{id}$/ === ide.message) ||
171
- (/^Couldn't look up columns$/ === ide.message) ||
172
- (/^Couldn't find series: #{class_key}\/#{id}$/ === ide.message)
173
-
174
- prior = nil
175
- end
176
- record = prior.nil? ? {} : prior.first.delete_if {|k,v| ["time", "sequence_number"].include?(k) }
177
233
  data.delete('time') if data.has_key?('time') && data['time'].nil?
178
- Zermelo.influxdb.write_point("#{class_key}/#{id}", record.merge(data).merge('id' => id))
234
+ Zermelo.influxdb.write_point("#{class_key}/#{id}", data.merge('id' => id))
179
235
  end
180
236
  end
181
237
 
@@ -1,26 +1,30 @@
1
- require 'zermelo/backends/base'
1
+ require 'zermelo/backend'
2
2
 
3
- require 'zermelo/filters/redis_filter'
3
+ require 'zermelo/filters/redis'
4
4
  require 'zermelo/locks/redis_lock'
5
5
 
6
6
  module Zermelo
7
7
 
8
8
  module Backends
9
9
 
10
- class RedisBackend
10
+ class Redis
11
11
 
12
- include Zermelo::Backends::Base
12
+ include Zermelo::Backend
13
13
 
14
- def default_sorted_set_key
15
- :timestamp
16
- end
14
+ # def default_sorted_set_key
15
+ # :timestamp
16
+ # end
17
17
 
18
18
  def generate_lock
19
19
  Zermelo::Locks::RedisLock.new
20
20
  end
21
21
 
22
- def filter(ids_key, record)
23
- Zermelo::Filters::RedisFilter.new(self, ids_key, record)
22
+ def filter(ids_key, associated_class, callback_target_class = nil,
23
+ callback_target_id = nil, callbacks = nil, sort_order = nil)
24
+
25
+ Zermelo::Filters::Redis.new(self, ids_key, associated_class,
26
+ callback_target_class, callback_target_id,
27
+ callbacks, sort_order)
24
28
  end
25
29
 
26
30
  def get_multiple(*attr_keys)
@@ -38,26 +42,25 @@ module Zermelo
38
42
  if attr_key.accessor.nil?
39
43
  Zermelo.redis.lrange(redis_attr_key, 0, -1)
40
44
  else
41
-
45
+ # TODO
42
46
  end
43
47
  when :set
44
48
  if attr_key.accessor.nil?
45
49
  Set.new( Zermelo.redis.smembers(redis_attr_key) )
46
50
  else
47
-
51
+ # TODO
48
52
  end
49
53
  when :hash
50
54
  if attr_key.accessor.nil?
51
55
  Zermelo.redis.hgetall(redis_attr_key)
52
56
  else
53
-
57
+ # TODO
54
58
  end
55
59
  when :sorted_set
56
- # TODO should this be something that preserves order?
57
60
  if attr_key.accessor.nil?
58
- Set.new( Zermelo.redis.zrange(redis_attr_key, 0, -1) )
61
+ Zermelo.redis.zrange(redis_attr_key, 0, -1)
59
62
  else
60
-
63
+ # TODO
61
64
  end
62
65
  end
63
66
 
@@ -94,19 +97,6 @@ module Zermelo
94
97
  end
95
98
  end
96
99
 
97
- def exists?(key)
98
- Zermelo.redis.exists(key_to_redis_key(key))
99
- end
100
-
101
- def include?(key, id)
102
- case key.type
103
- when :set
104
- Zermelo.redis.sismember(key_to_redis_key(key), id)
105
- else
106
- raise "Not implemented"
107
- end
108
- end
109
-
110
100
  def begin_transaction
111
101
  return false unless @transaction_redis.nil?
112
102
  @transaction_redis = Zermelo.redis
@@ -169,22 +159,11 @@ module Zermelo
169
159
  # TODO converge usage of idx_class and _index lookup invocation
170
160
 
171
161
  def index_lookup(att, associated_class, idx_class, value, attr_type, temp_keys)
172
- case value
173
- # when Zermelo::Filters::RedisFilter
174
-
175
- # TODO (maybe) if a filter from different backend, resolve to ids and
176
- # put that in a Redis temp set
177
-
178
- # collection, should_be_deleted = value.resolve_steps
179
-
180
- # if should_be_deleted
181
- # temp_sets << collection.name
182
- # end
183
-
184
- # unless :set.eql?(collection.type)
185
- # raise "Unsure as yet if non-sets are safe as Filter step values"
186
- # end
162
+ if (idx_class == Zermelo::Associations::RangeIndex) && !value.is_a?(Zermelo::Filters::IndexRange)
163
+ raise "Range index must be passed a range"
164
+ end
187
165
 
166
+ case value
188
167
  when Regexp
189
168
  raise "Can't query non-string values via regexp" unless :string.eql?(attr_type)
190
169
 
@@ -228,13 +207,31 @@ module Zermelo
228
207
  index = associated_class.send("#{att}_index")
229
208
 
230
209
  case index
210
+ when Zermelo::Associations::RangeIndex
211
+ r_index_key = key_to_redis_key(index.key)
212
+ range = if value.by_score
213
+ range_start = value.start.nil? ? '-inf' : safe_value(attr_type, value.start)
214
+ range_finish = value.finish.nil? ? '+inf' : safe_value(attr_type, value.finish)
215
+ Zermelo.redis.zrangebyscore(r_index_key, range_start, range_finish)
216
+ else
217
+ range_start = value.start || 0
218
+ range_finish = value.finish || -1
219
+ Zermelo.redis.zrange(r_index_key, range_start, range_finish)
220
+ end
221
+
222
+ # TODO another way for index_lookup to indicate 'empty result', rather
223
+ # than creating & returning an empty key
224
+ idx_key = associated_class.send(:temp_key, :set)
225
+ temp_keys << idx_key
226
+ Zermelo.redis.sadd(key_to_redis_key(idx_key), range) unless range.empty?
227
+ idx_key
231
228
  when Zermelo::Associations::UniqueIndex
232
229
  idx_key = associated_class.send(:temp_key, :set)
233
230
  temp_keys << idx_key
234
231
 
235
232
  Zermelo.redis.sadd(key_to_redis_key(idx_key),
236
233
  Zermelo.redis.hget(key_to_redis_key(index.key),
237
- index_keys(attr_type, value).join(':')))
234
+ index_keys(attr_type, value).join(':')))
238
235
  idx_key
239
236
  when Zermelo::Associations::Index
240
237
  index.key(value)
@@ -244,8 +241,8 @@ module Zermelo
244
241
 
245
242
  private
246
243
 
247
- def change(op, key, value = nil, key_to = nil)
248
- ch = [op, key, value, key_to]
244
+ def change(op, key, value = nil, key_to = nil, value_to = nil)
245
+ ch = [op, key, value, key_to, value_to]
249
246
  if @in_transaction
250
247
  @changes << ch
251
248
  else
@@ -259,10 +256,11 @@ module Zermelo
259
256
  purges = []
260
257
 
261
258
  changes.each do |ch|
262
- op = ch[0]
263
- key = ch[1]
264
- value = ch[2]
265
- key_to = ch[3]
259
+ op = ch[0]
260
+ key = ch[1]
261
+ value = ch[2]
262
+ key_to = ch[3]
263
+ value_to = ch[4]
266
264
 
267
265
  # TODO check that collection types handle nil value for whole thing
268
266
  if Zermelo::COLLECTION_TYPES.has_key?(key.type)
@@ -302,16 +300,14 @@ module Zermelo
302
300
  when :set
303
301
  Zermelo.redis.smove(complex_attr_key, key_to_redis_key(key_to), value)
304
302
  when :list
305
- # TODO would do via sort 'nosort', except for
306
- # https://github.com/antirez/redis/issues/2079 -- instead,
307
- # copy the workaround from redis_filter.rb
303
+ # TODO via sort 'nosort', or the workaround required prior to
304
+ # https://github.com/antirez/redis/issues/2079
308
305
  raise "Not yet implemented"
309
306
  when :hash
310
- values = value.to_a.flatten
311
- Zermelo.redis.hdel(complex_attr_key, values)
312
- Zermelo.redis.hset(key_to_redis_key(key_to), *values)
307
+ Zermelo.redis.hdel(complex_attr_key, *value.keys)
308
+ Zermelo.redis.hset(key_to_redis_key(key_to), *value_to.to_a.flatten)
313
309
  when :sorted_set
314
- raise "Not yet implemented"
310
+ Zermelo.redis.zadd(complex_attr_key, *value_to)
315
311
  end
316
312
  when :delete
317
313
  case key.type
@@ -329,7 +325,6 @@ module Zermelo
329
325
  end
330
326
 
331
327
  elsif :purge.eql?(op)
332
- # TODO get keys for all assocs & indices, purge them too
333
328
  purges << ["#{key.klass.send(:class_key)}:#{key.id}:attrs"]
334
329
  else
335
330
  simple_attr_key = key_to_redis_key(key)
@@ -0,0 +1,43 @@
1
+ require 'zermelo/backend'
2
+
3
+ module Zermelo
4
+
5
+ module Backends
6
+
7
+ class Stub
8
+
9
+ include Zermelo::Backend
10
+
11
+ def filter(ids_key, associated_class, callback_target_class = nil,
12
+ callback_target_id = nil, callbacks = nil, sort_order = nil)
13
+
14
+ raise "Not supported"
15
+ end
16
+
17
+ def get_multiple(*attr_keys)
18
+ raise "Not supported"
19
+ end
20
+
21
+ def begin_transaction
22
+ raise "Not supported"
23
+ end
24
+
25
+ def commit_transaction
26
+ raise "Not supported"
27
+ end
28
+
29
+ def abort_transaction
30
+ raise "Not supported"
31
+ end
32
+
33
+ private
34
+
35
+ def change(op, key, value = nil, key_to = nil, value_to = nil)
36
+ # no-op
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,194 @@
1
+ require 'active_support/concern'
2
+
3
+ require 'zermelo/records/errors'
4
+
5
+ require 'zermelo/filters/steps/list_step'
6
+ require 'zermelo/filters/steps/set_step'
7
+ require 'zermelo/filters/steps/sort_step'
8
+
9
+ module Zermelo
10
+
11
+ module Filter
12
+
13
+ extend ActiveSupport::Concern
14
+
15
+ attr_reader :backend, :steps
16
+
17
+ # initial set a Zermelo::Record::Key object
18
+ # associated_class the class of the result record
19
+ # TODO hash for these params as it's getting too long
20
+ def initialize(data_backend, initial_key, associated_class,
21
+ callback_target_class = nil, callback_target_id = nil,
22
+ callbacks = nil, sort_order = nil,
23
+ ancestor = nil, step = nil)
24
+ @backend = data_backend
25
+ @initial_key = initial_key
26
+ @associated_class = associated_class
27
+ @callback_target_class = callback_target_class
28
+ @callback_target_id = callback_target_id
29
+ @callbacks = callbacks
30
+ @sort_order = sort_order
31
+ @steps = ancestor.nil? ? [] : ancestor.steps.dup
32
+ @steps << step unless step.nil?
33
+ end
34
+
35
+ def intersect(attrs = {})
36
+ self.class.new(@backend, @initial_key, @associated_class,
37
+ @callback_target_class, @callback_target_id, @callbacks, @sort_order,
38
+ self, ::Zermelo::Filters::Steps::SetStep.new({:op => :intersect}, attrs))
39
+ end
40
+
41
+ def union(attrs = {})
42
+ self.class.new(@backend, @initial_key, @associated_class,
43
+ @callback_target_class, @callback_target_id, @callbacks, @sort_order,
44
+ self, ::Zermelo::Filters::Steps::SetStep.new({:op => :union}, attrs))
45
+ end
46
+
47
+ def diff(attrs = {})
48
+ self.class.new(@backend, @initial_key, @associated_class,
49
+ @callback_target_class, @callback_target_id, @callbacks, @sort_order,
50
+ self, ::Zermelo::Filters::Steps::SetStep.new({:op => :diff}, attrs))
51
+ end
52
+
53
+ def sort(keys, opts = {})
54
+ self.class.new(@backend, @initial_key, @associated_class,
55
+ @callback_target_class, @callback_target_id, @callbacks, @sort_order,
56
+ self, ::Zermelo::Filters::Steps::SortStep.new({:keys => keys,
57
+ :desc => opts[:desc], :limit => opts[:limit],
58
+ :offset => opts[:offset]}, {})
59
+ )
60
+ end
61
+
62
+ def offset(amount, opts = {})
63
+ self.class.new(@backend, @initial_key, @associated_class,
64
+ @callback_target_class, @callback_target_id, @callbacks, @sort_order,
65
+ self, ::Zermelo::Filters::Steps::ListStep.new({:offset => amount,
66
+ :limit => opts[:limit]}, {}))
67
+ end
68
+
69
+ # (a different syntax to the above)
70
+ def page(num, opts = {})
71
+ per_page = opts[:per_page].to_i || 20
72
+ start = per_page * (num - 1)
73
+ self.class.new(@backend, @initial_key, @associated_class,
74
+ @callback_target_class, @callback_target_id, @callbacks, @sort_order,
75
+ self, ::Zermelo::Filters::Steps::ListStep.new({:offset => start,
76
+ :limit => per_page}, {}))
77
+ end
78
+
79
+ # step users
80
+ def exists?(e_id)
81
+ lock(false) { _exists?(e_id) }
82
+ end
83
+
84
+ def find_by_id(f_id)
85
+ lock { _find_by_id(f_id) }
86
+ end
87
+
88
+ def find_by_id!(f_id)
89
+ ret = find_by_id(f_id)
90
+ raise ::Zermelo::Records::Errors::RecordNotFound.new(@associated_class, f_id) if ret.nil?
91
+ ret
92
+ end
93
+
94
+ def find_by_ids(*f_ids)
95
+ lock { f_ids.collect {|f_id| _find_by_id(f_id) } }
96
+ end
97
+
98
+ def find_by_ids!(*f_ids)
99
+ ret = find_by_ids(*f_ids)
100
+ if ret.any? {|r| r.nil? }
101
+ raise ::Zermelo::Records::Errors::RecordsNotFound.new(@associated_class, f_ids - ret.compact.map(&:id))
102
+ end
103
+ ret
104
+ end
105
+
106
+ def ids
107
+ lock(false) { _ids }
108
+ end
109
+
110
+ def count
111
+ lock(false) { _count }
112
+ end
113
+
114
+ def empty?
115
+ lock(false) { _count == 0 }
116
+ end
117
+
118
+ def all
119
+ lock { _all }
120
+ end
121
+
122
+ def collect(&block)
123
+ lock { _ids.collect {|id| block.call(_load(id))} }
124
+ end
125
+ alias_method :map, :collect
126
+
127
+ def each(&block)
128
+ lock { _ids.each {|id| block.call(_load(id)) } }
129
+ end
130
+
131
+ def select(&block)
132
+ lock { _all.select {|obj| block.call(obj) } }
133
+ end
134
+ alias_method :find_all, :select
135
+
136
+ def reject(&block)
137
+ lock { _all.reject {|obj| block.call(obj)} }
138
+ end
139
+
140
+ def destroy_all
141
+ lock(*@associated_class.send(:associated_classes)) do
142
+ _all.each {|r| r.destroy }
143
+ end
144
+ end
145
+
146
+ def associated_ids_for(name, options = {})
147
+ data_type, type_klass = @associated_class.send(:with_association_data, name.to_sym) do |data|
148
+ [data.data_type, data.type_klass]
149
+ end
150
+
151
+ lock {
152
+ case data_type
153
+ when :belongs_to, :has_one
154
+ type_klass.send(:associated_ids_for, @backend, data_type,
155
+ @associated_class, name,
156
+ options[:inversed].is_a?(TrueClass), *_ids)
157
+ else
158
+ type_klass.send(:associated_ids_for, @backend, data_type,
159
+ @associated_class, name, *_ids)
160
+ end
161
+ }
162
+ end
163
+
164
+ protected
165
+
166
+ def lock(when_steps_empty = true, *klasses, &block)
167
+ return(block.call) if !when_steps_empty && @steps.empty?
168
+ klasses += [@associated_class] if !klasses.include?(@associated_class)
169
+ @backend.lock(*klasses, &block)
170
+ end
171
+
172
+ private
173
+
174
+ def _find_by_id(id)
175
+ if !id.nil? && _exists?(id)
176
+ _load(id.to_s)
177
+ else
178
+ nil
179
+ end
180
+ end
181
+
182
+ def _load(id)
183
+ object = @associated_class.new
184
+ object.load(id)
185
+ object
186
+ end
187
+
188
+ def _all
189
+ _ids.map {|id| _load(id) }
190
+ end
191
+
192
+ end
193
+
194
+ end
@@ -0,0 +1,22 @@
1
+ module Zermelo
2
+ module Filters
3
+ class IndexRange
4
+
5
+ attr_reader :start, :finish, :by_score
6
+
7
+ def initialize(start, finish, opts = {})
8
+ value_types = opts[:by_score] ? [Float, Date, Time, DateTime] : [Integer]
9
+ [start, finish].each do |v|
10
+ raise "Values must be #{value_types.join('/')}" unless v.nil? || value_types.any? {|vt| v.is_a?(vt)}
11
+ end
12
+ if !start.nil? && !finish.nil? && (start > finish)
13
+ raise "Start of range must be <= finish"
14
+ end
15
+ @start = start
16
+ @finish = finish
17
+ @by_score = opts[:by_score]
18
+ end
19
+
20
+ end
21
+ end
22
+ end