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