rod 0.6.2 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.travis.yml +1 -0
  2. data/README.rdoc +10 -9
  3. data/Rakefile +15 -5
  4. data/changelog.txt +18 -0
  5. data/features/append.feature +0 -2
  6. data/features/basic.feature +7 -7
  7. data/features/collection_proxy.feature +140 -0
  8. data/features/flat_indexing.feature +9 -8
  9. data/features/{fred.feature → persistence.feature} +5 -8
  10. data/features/{assoc_indexing.feature → relationship_indexing.feature} +36 -0
  11. data/features/segmented_indexing.feature +6 -6
  12. data/features/steps/collection_proxy.rb +89 -0
  13. data/features/steps/model.rb +15 -3
  14. data/features/steps/rod.rb +1 -1
  15. data/features/support/mocha.rb +16 -0
  16. data/features/update.feature +263 -0
  17. data/lib/rod.rb +10 -2
  18. data/lib/rod/abstract_database.rb +49 -111
  19. data/lib/rod/abstract_model.rb +26 -6
  20. data/lib/rod/collection_proxy.rb +235 -34
  21. data/lib/rod/constants.rb +1 -1
  22. data/lib/rod/database.rb +5 -6
  23. data/lib/rod/exception.rb +1 -1
  24. data/lib/rod/index/base.rb +97 -0
  25. data/lib/rod/index/flat_index.rb +72 -0
  26. data/lib/rod/index/segmented_index.rb +100 -0
  27. data/lib/rod/model.rb +172 -185
  28. data/lib/rod/reference_updater.rb +85 -0
  29. data/lib/rod/utils.rb +29 -0
  30. data/rod.gemspec +4 -1
  31. data/tests/migration_create.rb +33 -12
  32. data/tests/migration_migrate.rb +24 -7
  33. data/tests/migration_model1.rb +5 -0
  34. data/tests/migration_model2.rb +36 -0
  35. data/tests/migration_verify.rb +49 -42
  36. data/tests/missing_class_create.rb +21 -0
  37. data/tests/missing_class_verify.rb +20 -0
  38. data/tests/properties_order_create.rb +16 -0
  39. data/tests/properties_order_verify.rb +17 -0
  40. data/tests/unit/abstract_database.rb +13 -0
  41. data/tests/unit/model_tests.rb +3 -3
  42. data/utils/convert_index.rb +1 -1
  43. metadata +62 -18
  44. data/lib/rod/segmented_index.rb +0 -85
data/lib/rod.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  require 'inline'
2
2
  require 'english/inflect'
3
- require 'active_model'
3
+ require 'active_model/deprecated_error_methods'
4
+ require 'active_model/validator'
5
+ require 'active_model/naming'
6
+ require 'active_model/translation'
7
+ require 'active_model/validations'
8
+ require 'active_model/dirty'
4
9
  require 'active_support/dependencies'
5
10
 
6
11
  # XXX This should be done in a different way, since a library should not
@@ -17,6 +22,9 @@ require 'rod/join_element'
17
22
  require 'rod/cache'
18
23
  require 'rod/collection_proxy'
19
24
  require 'rod/model'
25
+ require 'rod/reference_updater'
20
26
  require 'rod/string_element'
21
27
  require 'rod/string_ex'
22
- require 'rod/segmented_index'
28
+ require 'rod/index/base'
29
+ require 'rod/index/flat_index'
30
+ require 'rod/index/segmented_index'
@@ -1,7 +1,7 @@
1
1
  require 'singleton'
2
2
  require 'yaml'
3
- require 'rod/segmented_index'
4
- require 'fileutils'
3
+ require 'rod/index/base'
4
+ require 'rod/utils'
5
5
 
6
6
  module Rod
7
7
  # This class implements the database abstraction, i.e. it
@@ -13,9 +13,14 @@ module Rod
13
13
  # a given model (set of classes).
14
14
  include Singleton
15
15
 
16
+ include Utils
17
+
16
18
  # The meta-data of the DataBase.
17
19
  attr_reader :metadata
18
20
 
21
+ # The path which the database instance is located on.
22
+ attr_reader :path
23
+
19
24
  # Initializes the classes linked with this database and the handler.
20
25
  def initialize
21
26
  @classes ||= self.special_classes
@@ -58,12 +63,7 @@ module Rod
58
63
  klass.send(:build_structure)
59
64
  remove_file(klass.path_for_data(@path))
60
65
  klass.indexed_properties.each do |property,options|
61
- path = klass.path_for_index(@path,property,options)
62
- if test(?d,path)
63
- remove_files(path + "*")
64
- elsif test(?f,path)
65
- remove_file(path)
66
- end
66
+ klass.index_for(property,options).destroy
67
67
  end
68
68
  next if special_class?(klass)
69
69
  remove_files_but(klass.inline_library)
@@ -116,17 +116,20 @@ module Rod
116
116
  end
117
117
  generate_c_code(@path, self.classes)
118
118
  @handler = _init_handler(@path)
119
+ metadata_copy = @metadata.dup
120
+ metadata_copy.delete("Rod")
119
121
  self.classes.each do |klass|
120
- meta = @metadata[klass.name]
122
+ meta = metadata_copy.delete(klass.name)
121
123
  if meta.nil?
122
124
  # new class
123
125
  next
124
126
  end
125
- unless klass.compatible?(meta,self) || options[:generate] || options[:migrate]
127
+ unless klass.compatible?(meta) || options[:generate] || options[:migrate]
126
128
  raise IncompatibleVersion.
127
129
  new("Incompatible definition of '#{klass.name}' class.\n" +
128
- "Database and runtime versions are different:\n" +
129
- " #{meta}\n #{klass.metadata(self)}")
130
+ "Database and runtime versions are different:\n " +
131
+ klass.difference(meta).
132
+ map{|e1,e2| "DB: #{e1} vs. RT: #{e2}"}.join("\n "))
130
133
  end
131
134
  set_count(klass,meta[:count])
132
135
  file_size = File.new(klass.path_for_data(@path)).size
@@ -142,15 +145,20 @@ module Rod
142
145
  set_page_count(new_class,pages)
143
146
  end
144
147
  end
148
+ if metadata_copy.size > 0
149
+ @handler = nil
150
+ raise DatabaseError.new("The following classes are missing in runtime:\n - " +
151
+ metadata_copy.keys.join("\n - "))
152
+ end
145
153
  _open(@handler)
146
154
  if options[:migrate]
147
155
  empty_data = "\0" * _page_size
148
156
  self.classes.each do |klass|
149
157
  next unless klass.to_s =~ LEGACY_RE
150
158
  new_class = klass.name.sub(LEGACY_RE,"").constantize
151
- old_metadata = klass.metadata(self)
159
+ old_metadata = klass.metadata
152
160
  old_metadata.merge!({:superclass => old_metadata[:superclass].sub(LEGACY_RE,"")})
153
- unless new_class.compatible?(old_metadata,self)
161
+ unless new_class.compatible?(old_metadata)
154
162
  File.open(new_class.path_for_data(@path),"w") do |out|
155
163
  send("_#{new_class.struct_name}_page_count",@handler).
156
164
  times{|i| out.print(empty_data)}
@@ -267,6 +275,18 @@ module Rod
267
275
  class_id, @handler)
268
276
  end
269
277
 
278
+ # Allocates space for polymorphic join elements.
279
+ def allocate_polymorphic_join_elements(size)
280
+ raise DatabaseError.new("Readonly database.") if readonly_data?
281
+ _allocate_polymorphic_join_elements(size,@handler)
282
+ end
283
+
284
+ # Allocates space for join elements.
285
+ def allocate_join_elements(size)
286
+ raise DatabaseError.new("Readonly database.") if readonly_data?
287
+ _allocate_join_elements(size,@handler)
288
+ end
289
+
270
290
  # Returns the string of given +length+ starting at given +offset+.
271
291
  # Options:
272
292
  # * +:skip_encoding+ - if set to +true+, the string is left as ASCII-8BIT
@@ -305,25 +325,6 @@ module Rod
305
325
  send("_#{klass.struct_name}_page_count=",@handler,value)
306
326
  end
307
327
 
308
- # Reads index of +field+ (with +options+) for +klass+.
309
- def read_index(klass,field,options)
310
- case options[:index]
311
- when :flat,true
312
- begin
313
- File.open(klass.path_for_index(@path,field,options)) do |input|
314
- return {} if input.size == 0
315
- return Marshal.load(input)
316
- end
317
- rescue Errno::ENOENT
318
- return {}
319
- end
320
- when :segmented
321
- return SegmentedIndex.new(klass.path_for_index(@path,field,options))
322
- else
323
- raise RodException.new("Invalid index type '#{options[:index]}'.")
324
- end
325
- end
326
-
327
328
  # Store index of +field+ (with +options+) of +klass+ in the database.
328
329
  # There are two types of indices:
329
330
  # * +:flat+ - marshalled index is stored in one file
@@ -331,68 +332,25 @@ module Rod
331
332
  def write_index(klass,property,options)
332
333
  raise DatabaseError.new("Readonly database.") if readonly_data?
333
334
  class_index = klass.index_for(property,options)
334
- # Only convert the index, without (re)storing the values.
335
- unless options[:convert]
336
- class_index.each do |key,ids|
337
- unless ids.is_a?(CollectionProxy)
338
- proxy = CollectionProxy.new(ids[1],self,ids[0],klass)
339
- else
340
- proxy = ids
341
- end
342
- offset = _allocate_join_elements(proxy.size,@handler)
343
- proxy.each_id.with_index do |rod_id,index|
344
- set_join_element_id(offset, index, rod_id)
345
- end
346
- class_index[key] = [offset,proxy.size]
347
- end
348
- end
349
- case options[:index]
350
- when :flat,true
351
- File.open(klass.path_for_index(@path,property,options),"w") do |out|
352
- out.puts(Marshal.dump(class_index))
353
- end
354
- when :segmented
355
- path = klass.path_for_index(@path,property,options)
356
- if class_index.is_a?(Hash)
357
- index = SegmentedIndex.new(path)
358
- class_index.each{|k,v| index[k] = v}
359
- else
360
- index = class_index
361
- end
362
- index.save
363
- index = nil
335
+ if options[:convert]
336
+ # Only convert the index, without (re)storing the values.
337
+ index = Index::Base.create(klass.path_for_index(@path,property),klass,options)
338
+ index.copy(class_index)
339
+ class_index = index
364
340
  else
365
- raise RodException.new("Invalid index type '#{options[:index]}'.")
341
+ class_index.each do |key,proxy|
342
+ proxy.save
343
+ end
366
344
  end
345
+ class_index.save
346
+ class_index = nil
367
347
  end
368
348
 
369
349
  # Store the object in the database.
370
350
  def store(klass,object)
371
351
  raise DatabaseError.new("Readonly database.") if readonly_data?
372
- new_object = (object.rod_id == 0)
373
- if new_object
352
+ if object.new?
374
353
  send("_store_" + klass.struct_name,object,@handler)
375
- # set fields' values
376
- object.class.fields.each do |name,options|
377
- # rod_id is set during _store
378
- object.update_field(name) unless name == "rod_id"
379
- end
380
- # set ids of objects referenced via singular associations
381
- object.class.singular_associations.each do |name,options|
382
- object.update_singular_association(name,object.send(name))
383
- end
384
- end
385
- # set ids of objects referenced via plural associations
386
- # TODO should be disabled, when there are no new elements
387
- object.class.plural_associations.each do |name,options|
388
- elements = object.send(name) || []
389
- if options[:polymorphic]
390
- offset = _allocate_polymorphic_join_elements(elements.size,@handler)
391
- else
392
- offset = _allocate_join_elements(elements.size,@handler)
393
- end
394
- object.update_count_and_offset(name,elements.size,offset)
395
- object.update_plural_association(name,elements)
396
354
  end
397
355
  end
398
356
 
@@ -430,7 +388,8 @@ module Rod
430
388
 
431
389
  # Retruns the path to the DB as a name of a directory.
432
390
  def canonicalize_path(path)
433
- path + "/" unless path[-1] == "/"
391
+ path += "/" unless path[-1] == "/"
392
+ path
434
393
  end
435
394
 
436
395
  # Special classes used by the database.
@@ -533,28 +492,6 @@ module Rod
533
492
  generate_classes(legacy_module)
534
493
  end
535
494
 
536
- # Removes single file.
537
- def remove_file(file_name)
538
- if test(?f,file_name)
539
- File.delete(file_name)
540
- puts "Removing #{file_name}" if $ROD_DEBUG
541
- end
542
- end
543
-
544
- # Remove all files matching the +pattern+.
545
- # If +skip+ given, the file with the given name is not deleted.
546
- def remove_files(pattern,skip=nil)
547
- Dir.glob(pattern).each do |file_name|
548
- remove_file(file_name) unless file_name == skip
549
- end
550
- end
551
-
552
- # Removes all files which are similar (i.e. are generated
553
- # by RubyInline for the same class) to +name+
554
- # excluding the file with exactly the name given.
555
- def remove_files_but(name)
556
- remove_files(name.sub(INLINE_PATTERN_RE,"*"),name)
557
- end
558
495
 
559
496
  # Writes the metadata to the database.yml file.
560
497
  def write_metadata
@@ -564,7 +501,8 @@ module Rod
564
501
  rod_data[:created_at] = self.metadata["Rod"][:created_at] || Time.now
565
502
  rod_data[:updated_at] = Time.now
566
503
  self.classes.each do |klass|
567
- metadata[klass.name] = klass.metadata(self)
504
+ metadata[klass.name] = klass.metadata
505
+ metadata[klass.name][:count] = self.count(klass)
568
506
  end
569
507
  File.open(@path + DATABASE_FILE,"w") do |out|
570
508
  out.puts(YAML::dump(metadata))
@@ -46,20 +46,40 @@ module Rod
46
46
  end
47
47
 
48
48
  # Returns meta-data (in the form of a hash) for the model.
49
- def self.metadata(database)
49
+ def self.metadata
50
50
  meta = {}
51
- meta[:count] = database.count(self)
52
51
  meta[:superclass] = self.superclass.name
53
52
  meta
54
53
  end
55
54
 
56
55
  # Checks if the +metadata+ are compatible with the class definition.
57
- def self.compatible?(metadata,database)
58
- self_metadata = self.metadata(database)
56
+ def self.compatible?(metadata)
57
+ self.difference(metadata).empty?
58
+ end
59
+
60
+ # Calculates the difference between the classes metadata
61
+ # and the +metadata+ provided.
62
+ def self.difference(metadata)
63
+ my_metadata = self.metadata
59
64
  other_metadata = metadata.dup
60
- self_metadata.delete(:count)
61
65
  other_metadata.delete(:count)
62
- self_metadata == other_metadata
66
+ result = []
67
+ my_metadata.each do |type,values|
68
+ # TODO #161 the order of properties should be preserved for the
69
+ # whole class, not only for each type of properties.
70
+ if [:fields,:has_one,:has_many].include?(type)
71
+ values.to_a.zip(other_metadata[type].to_a) do |meta1,meta2|
72
+ if meta1 != meta2
73
+ result << [meta2,meta1]
74
+ end
75
+ end
76
+ else
77
+ if other_metadata[type] != values
78
+ result << [other_metadata[type],values]
79
+ end
80
+ end
81
+ end
82
+ result
63
83
  end
64
84
 
65
85
  end
@@ -1,26 +1,35 @@
1
+ require 'bsearch'
2
+ require 'rod/reference_updater'
3
+
1
4
  module Rod
2
- # This class allows for lazy fetching the objects from
3
- # a collection of Rod objects. It holds only a Ruby proc, which
4
- # called returns the object with given index.
5
+ # This class allows for lazy fetching the elements from
6
+ # a collection of Rod objects.
5
7
  class CollectionProxy
6
8
  include Enumerable
7
- attr_reader :size
9
+ attr_reader :size, :offset
8
10
  alias count size
9
11
 
10
- # Intializes the proxy with +size+ of the collection
11
- # and +fetch+ block for retrieving the object from the database.
12
+ # Intializes the proxy with its +size+, +database+ it is connected
13
+ # to, the +offset+ of join elements and the +klass+ of stored
14
+ # objects. If the klass is nil, the collection holds polymorphic
15
+ # objects.
12
16
  def initialize(size,database,offset,klass)
13
17
  @size = size
14
18
  @original_size = size
15
19
  @database = database
16
20
  @klass = klass
17
21
  @offset = offset
18
- @appended = []
22
+ #@commands = []
23
+ @added = []
24
+ @deleted = []
25
+ @map = {}
19
26
  end
20
27
 
21
28
  # Returns an object with given +index+.
29
+ # The +index+ have to be positive and smaller from the collection size.
30
+ # Otherwise +nil+ is returned.
22
31
  def [](index)
23
- return nil if index >= @size
32
+ return nil if index >= @size || index < 0
24
33
  rod_id = id_for(index)
25
34
  if rod_id.is_a?(Model)
26
35
  rod_id
@@ -33,14 +42,98 @@ module Rod
33
42
 
34
43
  # Appends element to the end of the collection.
35
44
  def <<(element)
36
- if element.rod_id == 0
37
- @appended << [element,element.class]
45
+ if element.nil?
46
+ pair = [0,NilClass]
38
47
  else
39
- @appended << [element.rod_id,element.class]
48
+ if element.new?
49
+ pair = [element,element.class]
50
+ else
51
+ pair = [element.rod_id,element.class]
52
+ end
40
53
  end
54
+ index = @size
55
+ @map[index] = @added.size
56
+ @added << pair
57
+ #@commands << [:append, pair]
41
58
  @size += 1
42
59
  end
43
60
 
61
+ # Inserts the +element+ at given +index+.
62
+ # So far the +index+ has to be positive, smaller or equal to size
63
+ # and only one pair of values is accepted. If these assumptions
64
+ # are not met, nil is returned.
65
+ def insert(index,element)
66
+ return nil if index < 0 || index > @size
67
+ if element.new?
68
+ pair = [element,element.class]
69
+ else
70
+ pair = [element.rod_id,element.class]
71
+ end
72
+ @map.keys.sort.reverse.each do |key|
73
+ if key >= index
74
+ value = @map.delete(key)
75
+ @map[key+1] = value
76
+ end
77
+ end
78
+ @map[index] = @added.size
79
+ @added << pair
80
+ #@commands << [:insert,pair]
81
+ @size += 1
82
+ self
83
+ end
84
+
85
+ # Removes the +element+ from the collection.
86
+ def delete(element)
87
+ indices = []
88
+ self.each.with_index{|e,i| indices << i if e == element}
89
+ if indices.empty?
90
+ if block_given?
91
+ return yield
92
+ else
93
+ return nil
94
+ end
95
+ end
96
+ #@commands << [:delete,indices]
97
+ indices.each.with_index do |index,offset|
98
+ self.delete_at(index-offset)
99
+ end
100
+ element
101
+ end
102
+
103
+ # Removes the element at +index+ from the colelction.
104
+ # So far the +index+ has to be positive.
105
+ def delete_at(index)
106
+ return nil if index >= @size || index < 0
107
+ element = self[index]
108
+ if direct_index = @map[index]
109
+ @added.delete_at(direct_index)
110
+ @map.delete(index)
111
+ @map.keys.sort.each do |key|
112
+ if key > index
113
+ value = @map.delete(key)
114
+ value -= 1 if value > direct_index
115
+ @map[key-1] = value
116
+ else
117
+ if (value = @map[key]) > direct_index
118
+ @map[key] -= 1
119
+ end
120
+ end
121
+ end
122
+ else
123
+ lazy_index = lazy_index(index)
124
+ position = @deleted.bsearch_upper_boundary{|e| e <=> lazy_index }
125
+ @deleted.insert(position,lazy_index)
126
+ @map.keys.sort.each do |key|
127
+ if key > index
128
+ @map[key-1] = @map.delete(key)
129
+ end
130
+ end
131
+ end
132
+ #@commands << [:delete,[index]]
133
+ @size -= 1
134
+ element
135
+ end
136
+
44
137
  # Simple each implementation.
45
138
  def each
46
139
  if block_given?
@@ -52,50 +145,158 @@ module Rod
52
145
  end
53
146
  end
54
147
 
55
- # Iterate over the rod_ids.
56
- def each_id
57
- if block_given?
58
- @size.times do |index|
59
- id = id_for(index)
60
- if id.is_a?(Model)
61
- raise IdException.new(id)
62
- else
63
- yield id
148
+ # Returns a collection of added items.
149
+ def added
150
+ @added.map do |id_or_object,klass|
151
+ if id_or_object.is_a?(Model)
152
+ id_or_object
153
+ else
154
+ id_or_object == 0 ? nil : klass.find_by_rod_id(id_or_object)
155
+ end
156
+ end
157
+ end
158
+
159
+ # Returns a collection of deleted items.
160
+ def deleted
161
+ @deleted.map do |index|
162
+ if polymorphic?
163
+ rod_id = @database.polymorphic_join_index(@offset,index)
164
+ if rod_id != 0
165
+ klass = Model.get_class(@database.polymorphic_join_class(@offset,index))
64
166
  end
167
+ else
168
+ klass = @klass
169
+ rod_id = @database.join_index(@offset,index)
65
170
  end
66
- else
67
- enum_for(:each_id)
171
+ rod_id == 0 ? nil : klass.find_by_rod_id(rod_id)
68
172
  end
69
173
  end
70
174
 
71
- # String representation.
175
+ # String representation of the collection proxy. Displays only the actual
176
+ # and the original size.
72
177
  def to_s
73
- "Proxy:[#{@size}][#{@original_size}]"
178
+ "Collection:[#{@size}][#{@original_size}]"
179
+ end
180
+
181
+ # Returns true if the collection is empty.
182
+ def empty?
183
+ self.count == 0
184
+ end
185
+
186
+ # Saves to collection proxy into disk and returns the collection
187
+ # proxy's +offset+.
188
+ # If no element was added nor deleted, nothing happes.
189
+ def save
190
+ unless @added.empty? && @deleted.empty?
191
+ # We cannot reuse the allocated space, since the data
192
+ # that is copied would be destroyed.
193
+ if polymorphic?
194
+ offset = @database.allocate_polymorphic_join_elements(@size)
195
+ else
196
+ offset = @database.allocate_join_elements(@size)
197
+ end
198
+ pairs =
199
+ @size.times.map do |index|
200
+ rod_id = id_for(index)
201
+ if rod_id.is_a?(Model)
202
+ object = rod_id
203
+ if object.new?
204
+ if polymorphic?
205
+ object.reference_updaters <<
206
+ ReferenceUpdater.for_plural(self,index,@database)
207
+ else
208
+ object.reference_updaters <<
209
+ ReferenceUpdater.for_plural(self,index,@database)
210
+ end
211
+ next
212
+ else
213
+ rod_id = object.rod_id
214
+ end
215
+ end
216
+ [rod_id,index]
217
+ end.compact
218
+ if polymorphic?
219
+ pairs.each do |rod_id,index|
220
+ class_id = (rod_id == 0 ? 0 : class_for(index).name_hash)
221
+ @database.set_polymorphic_join_element_id(offset,index,rod_id,class_id)
222
+ end
223
+ else
224
+ pairs.each do |rod_id,index|
225
+ @database.set_join_element_id(offset,index,rod_id)
226
+ end
227
+ end
228
+ @offset = offset
229
+ @added.clear
230
+ @deleted.clear
231
+ @map.clear
232
+ @original_size = @size
233
+ end
234
+ @offset
74
235
  end
75
236
 
76
237
  protected
238
+ # Returns true if the collection proxy is polymorphic, i.e. each
239
+ # element in the collection might be an instance of a different class.
240
+ def polymorphic?
241
+ @klass.nil?
242
+ end
243
+
244
+ # Updates in the database the +rod_id+ of the referenced object,
245
+ # which is stored at given +index+.
246
+ def update_reference_id(rod_id,index)
247
+ if polymorphic?
248
+ class_id = object.class.name_hash
249
+ @database.set_polymorphic_join_element_id(@offset, index, rod_id, class_id)
250
+ else
251
+ @database.set_join_element_id(@offset, index, rod_id)
252
+ end
253
+ end
254
+
255
+ # Returns the +rod_id+ of the element for given +index+. The
256
+ # id is taken from the DB or from in-memory map, depending
257
+ # on the fact if the collection were modified.
77
258
  def id_for(index)
78
- if index >= @original_size && !@appended[index - @original_size].nil?
79
- @appended[index - @original_size][0]
259
+ if direct_index = @map[index]
260
+ @added[direct_index][0]
80
261
  else
81
- if @klass.nil?
82
- @database.polymorphic_join_index(@offset,index)
262
+ if polymorphic?
263
+ @database.polymorphic_join_index(@offset,lazy_index(index))
83
264
  else
84
- @database.join_index(@offset,index)
265
+ @database.join_index(@offset,lazy_index(index))
85
266
  end
86
267
  end
87
268
  end
88
269
 
270
+ # Returns the +class_id+ of the element for given +index+. The
271
+ # id is taken from the DB or from in-memory map, depending
272
+ # on the fact if the collection were modified.
89
273
  def class_for(index)
90
- if index >= @original_size && !@appended[index - @original_size].nil?
91
- @appended[index - @original_size][1]
274
+ if polymorphic?
275
+ if direct_index = @map[index]
276
+ @added[direct_index][1]
277
+ else
278
+ Model.get_class(@database.polymorphic_join_class(@offset,lazy_index(index)))
279
+ end
92
280
  else
93
- if @klass.nil?
94
- Model.get_class(@database.polymorphic_join_class(@offset,index))
281
+ @klass
282
+ end
283
+ end
284
+
285
+ # Returns the index in the database corresponding to the given
286
+ # +index+ of the collection.
287
+ def lazy_index(index)
288
+ index -= @map.keys.select{|e| e < index}.size
289
+ result = 0
290
+ @deleted.each do |deleted_index|
291
+ if deleted_index - result > index
292
+ return result + index
95
293
  else
96
- @klass
294
+ index -= deleted_index - result
295
+ result = deleted_index + 1
97
296
  end
98
297
  end
298
+ result + index
99
299
  end
100
300
  end
101
301
  end
302
+