rod 0.6.2 → 0.6.3

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 (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
+