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
@@ -1,5 +1,5 @@
1
1
  module Rod
2
- VERSION = "0.6.2"
2
+ VERSION = "0.6.3"
3
3
 
4
4
  # The name of file containing the data base.
5
5
  DATABASE_FILE = "database.yml"
@@ -159,7 +159,7 @@ module Rod
159
159
  def inline_library
160
160
  unless defined?(@inline_library)
161
161
  self.class.inline(:C) do |builder|
162
- builder.c_singleton("void __unused_method_#{rand(1000)}(){}")
162
+ builder.c_singleton("void __unused_method_#{rand(1000000)}(){}")
163
163
 
164
164
  self.instance_variable_set("@inline_library",builder.so_name)
165
165
  end
@@ -355,7 +355,7 @@ module Rod
355
355
  | #{model_struct} * model_p;
356
356
  | unsigned long length = RSTRING_LEN(ruby_value);
357
357
  | char * value = RSTRING_PTR(ruby_value);
358
- | unsigned long string_offset, page_offset, current_page, sum;
358
+ | unsigned long string_offset, page_offset, current_page;
359
359
  | char * dest;
360
360
  | // table:
361
361
  | // - address of the first page
@@ -373,13 +373,12 @@ module Rod
373
373
  | page_offset = model_p->#{StringElement.struct_name}_count / page_size();
374
374
  | current_page = page_offset;
375
375
  | while(length_left > 0){
376
- | sum = ((unsigned long)length_left) + string_offset;
377
- | if(sum >= page_size()){
376
+ | if(((unsigned long)length_left) + string_offset >= page_size()){
378
377
  | \n#{mmap_class(StringElement)}
379
378
  | }
380
379
  | dest = model_p->#{StringElement.struct_name}_table +
381
380
  | current_page * page_size() + string_offset;
382
- | if(length_left > page_size()){
381
+ | if(((unsigned long)length_left) > page_size()){
383
382
  | memcpy(dest,value,page_size());
384
383
  | } else {
385
384
  | memcpy(dest,value,length_left);
@@ -578,7 +577,7 @@ module Rod
578
577
  if @@rod_development_mode
579
578
  # This method is created to force rebuild of the C code, since
580
579
  # it is rebuild on the basis of methods' signatures change.
581
- builder.c_singleton("void __unused_method_#{rand(1000)}(){}")
580
+ builder.c_singleton("void __unused_method_#{rand(1000000)}(){}")
582
581
  end
583
582
 
584
583
  # This has to be at the very end of builder definition!
@@ -61,7 +61,7 @@ module Rod
61
61
  attr_reader :object
62
62
 
63
63
  def initialize(message,object)
64
- super("The object has not been stored in the DB and its rod_id == 0")
64
+ super("The object has not been stored in the DB.")
65
65
  @object = object
66
66
  end
67
67
  end
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+ require 'rod/utils'
3
+
4
+ module Rod
5
+ module Index
6
+ # Base class for index implementations. It provides only a method
7
+ # for accessing the index by keys, but doesn't allow to set values
8
+ # for keys, since the kind of a value is a collection (proxy) of
9
+ # objects, that are indexed via given key. The returned collection
10
+ # allows for adding and removing the indexed objects.
11
+ #
12
+ # The implementing classes have to provide +get+ and +set+ methods,
13
+ # which are used to retrive and assign the values respectively.
14
+ class Base
15
+ include Utils
16
+ # Sets the class this index belongs to.
17
+ def initialize(klass)
18
+ @klass = klass
19
+ @unstored_map = {}
20
+ end
21
+
22
+ # Returns the collection of objects indexed by given +key+.
23
+ # The key might be a direct value (such as String) or a Rod object.
24
+ def [](key)
25
+ unstored_object = false
26
+ if key.is_a?(Model)
27
+ if key.new?
28
+ proxy = @unstored_map[key]
29
+ unstored_object = true
30
+ else
31
+ # TODO #155, the problem is how to determine the name_hash,
32
+ # when the class is generated in different module
33
+ # key = [key.rod_id,key.class.name_hash]
34
+ key = key.rod_id
35
+ proxy = get(key)
36
+ end
37
+ else
38
+ proxy = get(key)
39
+ end
40
+ if proxy.nil?
41
+ proxy = CollectionProxy.new(0,@klass.database,nil,@klass)
42
+ else
43
+ unless proxy.is_a?(CollectionProxy)
44
+ offset, count = proxy
45
+ proxy = CollectionProxy.new(count,@klass.database,offset,@klass)
46
+ end
47
+ end
48
+ if unstored_object
49
+ key.reference_updaters << ReferenceUpdater.for_index(self)
50
+ @unstored_map[key] = proxy
51
+ else
52
+ set(key,proxy)
53
+ end
54
+ proxy
55
+ end
56
+
57
+ # Copies the values from +index+ to this index.
58
+ def copy(index)
59
+ index.each do |key,value|
60
+ self.set(key,value)
61
+ end
62
+ end
63
+
64
+ # Moves the association between an ustored +object+ from
65
+ # memory to the index.
66
+ def key_persisted(object)
67
+ proxy = @unstored_map.delete(object)
68
+ # the update for that object has been done
69
+ return if proxy.nil?
70
+ # TODO #155, the problem is how to determine the name_hash,
71
+ # when the class is generated in different module
72
+ # key = [key.rod_id,key.class.name_hash]
73
+ key = object.rod_id
74
+ set(key,proxy)
75
+ end
76
+
77
+ class << self
78
+ # Creats the proper instance of Index or one of its sublcasses.
79
+ # The +path+ is the path were the index is stored, while +index+ is the previous index instance.
80
+ # The +klass+ is the class given index belongs to.
81
+ # Options might include class-specific options.
82
+ def create(path,klass,options)
83
+ options = options.dup
84
+ type = options.delete(:index)
85
+ case type
86
+ when :flat
87
+ FlatIndex.new(path,klass,options)
88
+ when :segmented
89
+ SegmentedIndex.new(path,klass,options)
90
+ else
91
+ raise RodException.new("Invalid index type #{type}")
92
+ end
93
+ end
94
+ end
95
+ end # class Base
96
+ end # module Index
97
+ end # module Rod
@@ -0,0 +1,72 @@
1
+ # encoding: utf-8
2
+ require 'rod/index/base'
3
+
4
+ module Rod
5
+ module Index
6
+ # Class implementing segmented index, i.e. an index which allows for
7
+ # lazy loading of its pieces.
8
+ class FlatIndex < Base
9
+ # Creats the index with given +path+ for given +klass+.
10
+ # Options are not used in the case of FlatIndex.
11
+ def initialize(path,klass,options={})
12
+ super(klass)
13
+ @path = path + ".idx"
14
+ @index = nil
15
+ end
16
+
17
+ # Stores the index on disk.
18
+ def save
19
+ File.open(@path,"w") do |out|
20
+ proxy_index = {}
21
+ @index.each{|k,col| proxy_index[k] = [col.offset,col.size]}
22
+ out.puts(Marshal.dump(proxy_index))
23
+ end
24
+ end
25
+
26
+ # Destroys the index (removes it from the disk completely).
27
+ def destroy
28
+ remove_file(@path)
29
+ end
30
+
31
+ def each
32
+ load_index unless loaded?
33
+ if block_given?
34
+ @index.each_key do |key|
35
+ yield key, self[key]
36
+ end
37
+ else
38
+ enum_for(:each)
39
+ end
40
+ end
41
+
42
+ protected
43
+ def get(key)
44
+ load_index unless loaded?
45
+ @index[key]
46
+ end
47
+
48
+ def set(key,value)
49
+ load_index unless loaded?
50
+ @index[key] = value
51
+ end
52
+
53
+ def loaded?
54
+ !@index.nil?
55
+ end
56
+
57
+ def load_index
58
+ begin
59
+ File.open(@path) do |input|
60
+ if input.size == 0
61
+ @index = {}
62
+ else
63
+ @index = Marshal.load(input)
64
+ end
65
+ end
66
+ rescue Errno::ENOENT
67
+ @index = {}
68
+ end
69
+ end
70
+ end # class FlatIndex
71
+ end # module Index
72
+ end # module Rod
@@ -0,0 +1,100 @@
1
+ # encoding: utf-8
2
+ require 'rod/index/base'
3
+
4
+ module Rod
5
+ module Index
6
+ # Class implementing segmented index, i.e. an index which allows for
7
+ # lazy loading of its pieces.
8
+ class SegmentedIndex < Base
9
+ # Default number of buckats.
10
+ BUCKETS_COUNT = 1001
11
+ # Creats the index with given +path+, with the previous +index+ instance
12
+ # and the following +options+:
13
+ # * +:buckets_count+ - the number of buckets.
14
+ def initialize(path,klass,options={:buckets_count => BUCKETS_COUNT})
15
+ super(klass)
16
+ @path = path + "_idx/"
17
+ @buckets_count = options[:buckets_count] || BUCKETS_COUNT
18
+ @buckets_ceil = Math::log2(@buckets_count).ceil
19
+ @buckets = {}
20
+ end
21
+
22
+ # Stores the index at @path. Assumes the path exists.
23
+ def save
24
+ unless File.exist?(@path)
25
+ Dir.mkdir(@path)
26
+ end
27
+ @buckets.each do |bucket_number,hash|
28
+ File.open(path_for(bucket_number),"w") do |out|
29
+ proxy_index = {}
30
+ hash.each{|k,col| proxy_index[k] = [col.offset,col.size]}
31
+ out.puts(Marshal.dump(proxy_index))
32
+ end
33
+ end
34
+ end
35
+
36
+ # Destroys the index (removes it from the disk completely).
37
+ def destroy
38
+ remove_files(@path + "*")
39
+ end
40
+
41
+ def each
42
+ if block_given?
43
+ @buckets.each do |bucket_number,hash|
44
+ hash.each_key do |key|
45
+ yield key, self[key]
46
+ end
47
+ end
48
+ else
49
+ enum_for(:each)
50
+ end
51
+ end
52
+
53
+ protected
54
+ def get(key)
55
+ bucket_number = bucket_for(key)
56
+ load_bucket(bucket_number) unless @buckets[bucket_number]
57
+ @buckets[bucket_number][key]
58
+ end
59
+
60
+ def set(key,value)
61
+ bucket_number = bucket_for(key)
62
+ load_bucket(bucket_number) unless @buckets[bucket_number]
63
+ @buckets[bucket_number][key] = value
64
+ end
65
+
66
+ def bucket_for(key)
67
+ case key
68
+ when NilClass
69
+ 1 % @buckets_count
70
+ when TrueClass
71
+ 2 % @buckets_count
72
+ when FalseClass
73
+ 3 % @buckets_count
74
+ when String
75
+ key.sum(@buckets_ceil) % @buckets_count
76
+ when Integer
77
+ key % @buckets_count
78
+ when Float
79
+ (key.numerator - key.denominator) % @buckets_count
80
+ else
81
+ raise RodException.new("Object of type '#{key.class}' not supported as a key of segmented index!")
82
+ end
83
+ end
84
+
85
+ def path_for(bucket_number)
86
+ "#{@path}#{bucket_number}.idx"
87
+ end
88
+
89
+ def load_bucket(bucket_number)
90
+ if File.exist?(path_for(bucket_number))
91
+ File.open(path_for(bucket_number)) do |input|
92
+ @buckets[bucket_number] = Marshal.load(input)
93
+ end
94
+ else
95
+ @buckets[bucket_number] = {}
96
+ end
97
+ end
98
+ end # class SegmentedIndex
99
+ end # module Index
100
+ end # module Rod
@@ -7,14 +7,22 @@ module Rod
7
7
  # Abstract class representing a model entity. Each storable class has to derieve from +Model+.
8
8
  class Model < AbstractModel
9
9
  include ActiveModel::Validations
10
+ include ActiveModel::Dirty
10
11
  extend Enumerable
11
12
 
13
+ # A list of updaters that has to be notified when the +rod_id+
14
+ # of this object is defined. See ReferenceUpdater for details.
15
+ attr_reader :reference_updaters
16
+
12
17
  # If +options+ is an integer it is the @rod_id of the object.
13
18
  def initialize(options=nil)
19
+ @reference_updaters = []
14
20
  case options
15
21
  when Integer
16
22
  @rod_id = options
17
23
  when Hash
24
+ @rod_id = 0
25
+ initialize_fields
18
26
  options.each do |key,value|
19
27
  begin
20
28
  self.send("#{key}=",value)
@@ -22,12 +30,26 @@ module Rod
22
30
  raise RodException.new("There is no field or association with name #{key}!")
23
31
  end
24
32
  end
33
+ when NilClass
25
34
  @rod_id = 0
35
+ initialize_fields
26
36
  else
27
- @rod_id = 0
37
+ raise InvalidArgument.new("initialize(options)",options)
28
38
  end
29
39
  end
30
40
 
41
+ # Returns duplicated object, which shares the state of fields and
42
+ # associations, but is separatly persisted (has its own +rod_id+,
43
+ # dirty attributes, etc.).
44
+ # WARNING: This behaviour might change slightly in future #157
45
+ def dup
46
+ object = super()
47
+ object.instance_variable_set("@rod_id",0)
48
+ object.instance_variable_set("@reference_updaters",@reference_updaters.dup)
49
+ object.instance_variable_set("@changed_attributes",@changed_attributes.dup)
50
+ object
51
+ end
52
+
31
53
  #########################################################################
32
54
  # Public API
33
55
  #########################################################################
@@ -45,6 +67,36 @@ module Rod
45
67
  else
46
68
  self.class.store(self)
47
69
  end
70
+ # The default values doesn't have to be persisted, since they
71
+ # are returned by default by the accessors.
72
+ self.changed.each do |property|
73
+ property = property.to_sym
74
+ if self.class.field?(property)
75
+ # store field value
76
+ update_field(property)
77
+ elsif self.class.singular_association?(property)
78
+ # store singular association value
79
+ update_singular_association(property,send(property))
80
+ else
81
+ # Plural associations are not tracked.
82
+ raise RodException.new("Invalid changed property #{self.class}##{property}'")
83
+ end
84
+ end
85
+ # store plural associations in the DB
86
+ self.class.plural_associations.each do |property,options|
87
+ collection = send(property)
88
+ offset = collection.save
89
+ update_count_and_offset(property,collection.size,offset)
90
+ end
91
+ # notify reference updaters
92
+ reference_updaters.each do |updater|
93
+ updater.update(self)
94
+ end
95
+ reference_updaters.clear
96
+ # XXX we don't use the 'previously changed' feature, since the simplest
97
+ # implementation requires us to leave references to objects, which
98
+ # forbids them to be garbage collected.
99
+ @changed_attributes.clear unless @changed_attributes.nil?
48
100
  end
49
101
 
50
102
  # Default implementation of equality.
@@ -52,6 +104,11 @@ module Rod
52
104
  self.class == other.class && self.rod_id == other.rod_id
53
105
  end
54
106
 
107
+ # Returns +true+ if the object hasn't been persisted yet.
108
+ def new?
109
+ @rod_id == 0
110
+ end
111
+
55
112
  # Default implementation of +inspect+.
56
113
  def inspect
57
114
  fields = self.class.fields.map{|n,o| "#{n}:#{self.send(n)}"}.join(",")
@@ -65,6 +122,19 @@ module Rod
65
122
  self.inspect
66
123
  end
67
124
 
125
+ # Returns a hash {'attr_name' => 'attr_value'} which covers fields and
126
+ # has_one relationships values. This is required by ActiveModel::Dirty.
127
+ def attributes
128
+ result = {}
129
+ self.class.fields.each do |name,options|
130
+ result[name.to_s] = self.send(name)
131
+ end
132
+ self.class.singular_associations.each do |name,options|
133
+ result[name.to_s] = self.send(name)
134
+ end
135
+ result
136
+ end
137
+
68
138
  # Returns the number of objects of this class stored in the
69
139
  # database.
70
140
  def self.count
@@ -99,6 +169,29 @@ module Rod
99
169
  end
100
170
 
101
171
  protected
172
+ # Sets the default values for fields.
173
+ def initialize_fields
174
+ self.class.fields.each do |name,options|
175
+ next if name == "rod_id"
176
+ value =
177
+ case options[:type]
178
+ when :integer
179
+ 0
180
+ when :ulong
181
+ 0
182
+ when :float
183
+ 0.0
184
+ when :string
185
+ ''
186
+ when :object
187
+ nil
188
+ else
189
+ raise InvalidArgument.new(options[:type],"field type")
190
+ end
191
+ send("#{name}=",value)
192
+ end
193
+ end
194
+
102
195
  # A macro-style function used to indicate that given piece of data
103
196
  # is stored in the database.
104
197
  # Type should be one of:
@@ -167,8 +260,24 @@ module Rod
167
260
  public
168
261
  # Update the DB information about the +object+ which
169
262
  # is referenced via singular association with +name+.
263
+ # If the object is not yet stored, a reference updater
264
+ # is registered to update the DB when it is stored.
170
265
  def update_singular_association(name, object)
171
- rod_id = object.nil? ? 0 : object.rod_id
266
+ if object.nil?
267
+ rod_id = 0
268
+ else
269
+ if object.new?
270
+ # There is a referenced object, but its rod_id is not set.
271
+ object.reference_updaters << ReferenceUpdater.
272
+ for_singular(self,name,self.database)
273
+ return
274
+ else
275
+ rod_id = object.rod_id
276
+ end
277
+ # clear references, allowing for garbage collection
278
+ # WARNING: don't use writer, since we don't want this change to be tracked
279
+ #object.instance_variable_set("@#{name}",nil)
280
+ end
172
281
  send("_#{name}=", @rod_id, rod_id)
173
282
  if self.class.singular_associations[name][:polymorphic]
174
283
  class_id = object.nil? ? 0 : object.class.name_hash
@@ -176,46 +285,6 @@ module Rod
176
285
  end
177
286
  end
178
287
 
179
- # Update in the DB information about the +object+ (or objects) which is (are)
180
- # referenced via plural association with +name+.
181
- #
182
- # The name of the association is +name+, the referenced
183
- # object(s) is (are) +object+.
184
- # +index+ is the position of the referenced object in the association.
185
- # If there are many objects, the index is ignored.
186
- def update_plural_association(name, object, index=nil)
187
- offset = send("_#{name}_offset",@rod_id)
188
- if self.class.plural_associations[name][:polymorphic]
189
- # If you wish to refactor this code, ensure performance is preserved.
190
- if object.respond_to?(:each)
191
- objects = object
192
- objects.each.with_index do |object,index|
193
- rod_id = object.nil? ? 0 : object.rod_id
194
- class_id = object.nil? ? 0 : object.class.name_hash
195
- database.set_polymorphic_join_element_id(offset, index, rod_id,
196
- class_id)
197
- end
198
- else
199
- rod_id = object.nil? ? 0 : object.rod_id
200
- class_id = object.nil? ? 0 : object.class.name_hash
201
- database.set_polymorphic_join_element_id(offset, index, rod_id,
202
- class_id)
203
- end
204
- else
205
- # If you wish to refactor this code, ensure performance is preserved.
206
- if object.respond_to?(:each)
207
- objects = object
208
- objects.each.with_index do |object,index|
209
- rod_id = object.nil? ? 0 : object.rod_id
210
- database.set_join_element_id(offset, index, rod_id)
211
- end
212
- else
213
- rod_id = object.nil? ? 0 : object.rod_id
214
- database.set_join_element_id(offset, index, rod_id)
215
- end
216
- end
217
- end
218
-
219
288
  # Updates in the DB the +count+ and +offset+ of elements for +name+ association.
220
289
  def update_count_and_offset(name,count,offset)
221
290
  send("_#{name}_count=",@rod_id,count)
@@ -255,97 +324,34 @@ module Rod
255
324
  unless object.is_a?(self)
256
325
  raise RodException.new("Incompatible object class #{object.class}.")
257
326
  end
258
- new_object = (object.rod_id == 0)
327
+ stored_now = object.new?
259
328
  database.store(self,object)
260
329
  cache[object.rod_id] = object
261
330
 
262
- referenced_objects ||= database.referenced_objects
263
-
264
- # update indices
331
+ # update class indices
265
332
  indexed_properties.each do |property,options|
266
- # singular and plural associations with nil as value are not indexed
267
- keys =
268
- if new_object
269
- if field?(property)
270
- [object.send(property)]
271
- elsif singular_association?(property)
272
- [object.send(property)].compact
273
- else
274
- object.send(property).to_a.compact
275
- end
276
- elsif plural_association?(property)
277
- object.send(property).to_a.compact
278
- end
279
- next if keys.nil?
280
- keys.each.with_index do |key_or_object,key_index|
281
- key = (key_or_object.is_a?(Model) ? key_or_object.rod_id : key_or_object)
282
- proxy = self.index_for(property,options,key)
283
- if proxy.nil?
284
- proxy = self.set_values_for(property,options,key,0,database,nil)
285
- else
286
- unless proxy.is_a?(CollectionProxy)
287
- offset, count = proxy
288
- proxy = self.set_values_for(property,options,key,count,database,offset)
333
+ # WARNING: singular and plural associations with nil as value are not indexed!
334
+ # TODO #156 think over this constraint, write specs in persistence.feature
335
+ if field?(property) || singular_association?(property)
336
+ if stored_now || object.changes.has_key?(property)
337
+ unless stored_now
338
+ old_value = object.changes[property][0]
339
+ self.index_for(property,options)[old_value].delete(object)
289
340
  end
290
- end
291
- if new_object || plural_association?(property) && proxy[key_index].nil?
292
- if plural_association?(property) && key == 0
293
- # TODO #94 devise method for reference rebuilding
341
+ new_value = object.send(property)
342
+ if field?(property) || new_value
343
+ self.index_for(property,options)[new_value] << object
294
344
  end
295
- proxy << object
296
345
  end
297
- end
298
- end
299
-
300
- # update object that references the stored object
301
- # ... via singular associations
302
- singular_associations.each do |name, options|
303
- referenced = object.send(name)
304
- unless referenced.nil?
305
- # There is a referenced object, but its rod_id is not set.
306
- if referenced.rod_id == 0
307
- unless referenced_objects.has_key?(referenced)
308
- referenced_objects[referenced] = []
309
- end
310
- referenced_objects[referenced].push([object.rod_id, name,
311
- object.class.name_hash])
346
+ elsif plural_association?(property)
347
+ object.send(property).deleted.each do |deleted|
348
+ self.index_for(property,options)[deleted].delete(object) unless deleted.nil?
312
349
  end
313
- # clear references, allowing for garbage collection
314
- object.send("#{name}=",nil)
315
- end
316
- end
317
-
318
- # ... via plural associations
319
- plural_associations.each do |name, options|
320
- referenced = object.send(name)
321
- unless referenced.nil?
322
- referenced.each_with_index do |element, index|
323
- # There are referenced objects, but their rod_id is not set
324
- if !element.nil? && element.rod_id == 0
325
- unless referenced_objects.has_key?(element)
326
- referenced_objects[element] = []
327
- end
328
- referenced_objects[element].push([object.rod_id, name,
329
- object.class.name_hash, index])
330
- end
331
- end
332
- # clear references, allowing for garbage collection
333
- object.send("#{name}=",nil)
334
- end
335
- end
336
-
337
- reverse_references = referenced_objects.delete(object)
338
-
339
- unless reverse_references.blank?
340
- reverse_references.each do |referee_rod_id, method_name, class_id, index|
341
- referee = Model.get_class(class_id).find_by_rod_id(referee_rod_id)
342
- self.cache.delete(referee_rod_id)
343
- if index.nil?
344
- # singular association
345
- referee.update_singular_association(method_name, object)
346
- else
347
- referee.update_plural_association(method_name, object, index)
350
+ object.send(property).added.each do |added|
351
+ self.index_for(property,options)[added] << object unless added.nil?
348
352
  end
353
+ else
354
+ raise RodException.new("Unknown property type for #{self}##{property}")
349
355
  end
350
356
  end
351
357
  end
@@ -402,8 +408,8 @@ module Rod
402
408
  end
403
409
 
404
410
  # Metadata for the model class.
405
- def self.metadata(database)
406
- meta = super(database)
411
+ def self.metadata
412
+ meta = super
407
413
  # fields
408
414
  fields = meta[:fields] = {} unless self.fields.size == 1
409
415
  self.fields.each do |field,options|
@@ -517,6 +523,7 @@ module Rod
517
523
 
518
524
  # Used for establishing link with the DB.
519
525
  def self.inherited(subclass)
526
+ super
520
527
  subclass.add_to_class_space
521
528
  subclasses << subclass
522
529
  begin
@@ -675,16 +682,9 @@ module Rod
675
682
  end
676
683
 
677
684
  # The name of the file or directory (for given +relative_path+), which the
678
- # index of the +field+ (with +options+) of this class is stored in.
679
- def self.path_for_index(relative_path,field,options)
680
- case options[:index]
681
- when :flat,true
682
- "#{relative_path}#{model_path}_#{field}.idx"
683
- when :segmented
684
- "#{relative_path}#{model_path}_#{field}_idx/"
685
- else
686
- raise RodException.new("Invalid index type #{type}")
687
- end
685
+ # index of the +field+ of this class is stored in.
686
+ def self.path_for_index(relative_path,field)
687
+ "#{relative_path}#{model_path}_#{field}"
688
688
  end
689
689
 
690
690
  # Returns true if the type of the filed is string-like (i.e. stored as
@@ -808,7 +808,7 @@ module Rod
808
808
  if Database.development_mode
809
809
  # This method is created to force rebuild of the C code, since
810
810
  # it is rebuild on the basis of methods' signatures change.
811
- builder.c_singleton("void __unused_method_#{rand(1000)}(){}")
811
+ builder.c_singleton("void __unused_method_#{rand(1000000)}(){}")
812
812
  end
813
813
 
814
814
  self.fields.each do |name, options|
@@ -856,8 +856,10 @@ module Rod
856
856
  end
857
857
  end
858
858
 
859
+ attribute_methods = []
859
860
  ## accessors for fields, plural and singular relationships follow
860
861
  self.fields.each do |field, options|
862
+ attribute_methods << field
861
863
  # optimization
862
864
  field = field.to_s
863
865
  # adding new private fields visible from Ruby
@@ -873,7 +875,7 @@ module Rod
873
875
  define_method(field) do
874
876
  value = instance_variable_get("@#{field}")
875
877
  if value.nil?
876
- if @rod_id == 0
878
+ if self.new?
877
879
  value = nil
878
880
  else
879
881
  value = send("_#{field}",@rod_id)
@@ -885,6 +887,8 @@ module Rod
885
887
 
886
888
  # setter
887
889
  define_method("#{field}=") do |value|
890
+ old_value = send(field)
891
+ send("#{field}_will_change!") unless old_value == value
888
892
  instance_variable_set("@#{field}",value)
889
893
  value
890
894
  end
@@ -894,7 +898,7 @@ module Rod
894
898
  define_method(field) do
895
899
  value = instance_variable_get("@#{field}")
896
900
  if value.nil? # first call
897
- if @rod_id == 0
901
+ if self.new?
898
902
  return (options[:type] == :object ? nil : "")
899
903
  else
900
904
  length = send("_#{field}_length", @rod_id)
@@ -911,7 +915,8 @@ module Rod
911
915
  value = Marshal.load(value)
912
916
  end
913
917
  # caching Ruby representation
914
- send("#{field}=",value)
918
+ # don't use writer - avoid change tracking
919
+ instance_variable_set("@#{field}",value)
915
920
  end
916
921
  end
917
922
  value
@@ -919,6 +924,8 @@ module Rod
919
924
 
920
925
  # setter
921
926
  define_method("#{field}=") do |value|
927
+ old_value = send(field)
928
+ send("#{field}_will_change!") unless old_value == value
922
929
  instance_variable_set("@#{field}",value)
923
930
  end
924
931
  end
@@ -926,6 +933,7 @@ module Rod
926
933
  end
927
934
 
928
935
  singular_associations.each do |name, options|
936
+ attribute_methods << name
929
937
  # optimization
930
938
  name = name.to_s
931
939
  private "_#{name}", "_#{name}="
@@ -940,7 +948,7 @@ module Rod
940
948
  define_method(name) do
941
949
  value = instance_variable_get("@#{name}")
942
950
  if value.nil?
943
- return nil if @rod_id == 0
951
+ return nil if self.new?
944
952
  rod_id = send("_#{name}",@rod_id)
945
953
  # the indices are shifted by 1, to leave 0 for nil
946
954
  if rod_id == 0
@@ -953,13 +961,16 @@ module Rod
953
961
  value = class_name.constantize.find_by_rod_id(rod_id)
954
962
  end
955
963
  end
956
- send("#{name}=",value)
964
+ # avoid change tracking
965
+ instance_variable_set("@#{name}",value)
957
966
  end
958
967
  value
959
968
  end
960
969
 
961
970
  #setter
962
971
  define_method("#{name}=") do |value|
972
+ old_value = send(name)
973
+ send("#{name}_will_change!") unless old_value == value
963
974
  instance_variable_set("@#{name}", value)
964
975
  end
965
976
  end
@@ -979,13 +990,13 @@ module Rod
979
990
  define_method("#{name}") do
980
991
  proxy = instance_variable_get("@#{name}")
981
992
  if proxy.nil?
982
- if @rod_id == 0
993
+ if self.new?
983
994
  count = 0
995
+ offset = 0
984
996
  else
985
997
  count = self.send("_#{name}_count",@rod_id)
998
+ offset = self.send("_#{name}_offset",@rod_id)
986
999
  end
987
- return instance_variable_set("@#{name}",[]) if count == 0
988
- offset = self.send("_#{name}_offset",@rod_id)
989
1000
  proxy = CollectionProxy.new(count,database,offset,klass)
990
1001
  instance_variable_set("@#{name}", proxy)
991
1002
  end
@@ -997,7 +1008,7 @@ module Rod
997
1008
  if (instance_variable_get("@#{name}") != nil)
998
1009
  return instance_variable_get("@#{name}").count
999
1010
  else
1000
- if @rod_id == 0
1011
+ if self.new?
1001
1012
  return 0
1002
1013
  else
1003
1014
  return send("_#{name}_count",@rod_id)
@@ -1007,10 +1018,17 @@ module Rod
1007
1018
 
1008
1019
  # setter
1009
1020
  define_method("#{name}=") do |value|
1010
- instance_variable_set("@#{name}", value)
1021
+ proxy = send(name)
1022
+ value.each do |object|
1023
+ proxy << object
1024
+ end
1025
+ proxy
1011
1026
  end
1012
1027
  end
1013
1028
 
1029
+ # dirty tracking
1030
+ define_attribute_methods(attribute_methods)
1031
+
1014
1032
  # indices
1015
1033
  indexed_properties.each do |property,options|
1016
1034
  # optimization
@@ -1018,31 +1036,12 @@ module Rod
1018
1036
  (class << self; self; end).class_eval do
1019
1037
  # Find all objects with given +value+ of the +property+.
1020
1038
  define_method("find_all_by_#{property}") do |value|
1021
- value = value.rod_id if value.is_a?(Model)
1022
- proxy = index_for(property,options,value)
1023
- if proxy.is_a?(CollectionProxy)
1024
- proxy
1025
- else
1026
- offset,count = proxy
1027
- return [] if offset.nil?
1028
- CollectionProxy.new(count,database,offset,self)
1029
- end
1039
+ index_for(property,options)[value]
1030
1040
  end
1031
1041
 
1032
1042
  # Find first object with given +value+ of the +property+.
1033
1043
  define_method("find_by_#{property}") do |value|
1034
- value = value.rod_id if value.is_a?(Model)
1035
- proxy = index_for(property,options)[value]
1036
- if proxy.is_a?(CollectionProxy)
1037
- proxy[0]
1038
- else
1039
- offset,count = proxy
1040
- if offset.nil?
1041
- nil
1042
- else
1043
- get(database.join_index(offset,0))
1044
- end
1045
- end
1044
+ index_for(property,options)[value][0]
1046
1045
  end
1047
1046
  end
1048
1047
  end
@@ -1076,27 +1075,14 @@ module Rod
1076
1075
  end
1077
1076
 
1078
1077
  # Read index for the +property+ with +options+ from the database.
1079
- # If +key+ is given, the value for the key is returned.
1080
- # accessing the values for that key.
1081
- def index_for(property,options,key=nil)
1078
+ def index_for(property,options)
1082
1079
  index = instance_variable_get("@#{property}_index")
1083
1080
  if index.nil?
1084
- index = database.read_index(self,property,options)
1081
+ path = path_for_index(database.path,property)
1082
+ index = Index::Base.create(path,self,options)
1085
1083
  instance_variable_set("@#{property}_index",index)
1086
1084
  end
1087
- if key
1088
- index[key]
1089
- else
1090
- index
1091
- end
1092
- end
1093
-
1094
- # Sets the values in the index of the +property+ for
1095
- # the particular +key+. Method expects +fetch+ block and
1096
- # creates a CollectionProxy based on that block.
1097
- # The size of the collection is given as +count+.
1098
- def set_values_for(property,options,key,count,database,offset)
1099
- index_for(property,options)[key] = CollectionProxy.new(count,database,offset,self)
1085
+ index
1100
1086
  end
1101
1087
 
1102
1088
  private
@@ -1114,3 +1100,4 @@ module Rod
1114
1100
  end
1115
1101
  end
1116
1102
  end
1103
+