rod 0.6.2 → 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -0
- data/README.rdoc +10 -9
- data/Rakefile +15 -5
- data/changelog.txt +18 -0
- data/features/append.feature +0 -2
- data/features/basic.feature +7 -7
- data/features/collection_proxy.feature +140 -0
- data/features/flat_indexing.feature +9 -8
- data/features/{fred.feature → persistence.feature} +5 -8
- data/features/{assoc_indexing.feature → relationship_indexing.feature} +36 -0
- data/features/segmented_indexing.feature +6 -6
- data/features/steps/collection_proxy.rb +89 -0
- data/features/steps/model.rb +15 -3
- data/features/steps/rod.rb +1 -1
- data/features/support/mocha.rb +16 -0
- data/features/update.feature +263 -0
- data/lib/rod.rb +10 -2
- data/lib/rod/abstract_database.rb +49 -111
- data/lib/rod/abstract_model.rb +26 -6
- data/lib/rod/collection_proxy.rb +235 -34
- data/lib/rod/constants.rb +1 -1
- data/lib/rod/database.rb +5 -6
- data/lib/rod/exception.rb +1 -1
- data/lib/rod/index/base.rb +97 -0
- data/lib/rod/index/flat_index.rb +72 -0
- data/lib/rod/index/segmented_index.rb +100 -0
- data/lib/rod/model.rb +172 -185
- data/lib/rod/reference_updater.rb +85 -0
- data/lib/rod/utils.rb +29 -0
- data/rod.gemspec +4 -1
- data/tests/migration_create.rb +33 -12
- data/tests/migration_migrate.rb +24 -7
- data/tests/migration_model1.rb +5 -0
- data/tests/migration_model2.rb +36 -0
- data/tests/migration_verify.rb +49 -42
- data/tests/missing_class_create.rb +21 -0
- data/tests/missing_class_verify.rb +20 -0
- data/tests/properties_order_create.rb +16 -0
- data/tests/properties_order_verify.rb +17 -0
- data/tests/unit/abstract_database.rb +13 -0
- data/tests/unit/model_tests.rb +3 -3
- data/utils/convert_index.rb +1 -1
- metadata +62 -18
- data/lib/rod/segmented_index.rb +0 -85
data/lib/rod/constants.rb
CHANGED
data/lib/rod/database.rb
CHANGED
@@ -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(
|
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
|
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
|
-
|
|
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(
|
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!
|
data/lib/rod/exception.rb
CHANGED
@@ -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
|
data/lib/rod/model.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
327
|
+
stored_now = object.new?
|
259
328
|
database.store(self,object)
|
260
329
|
cache[object.rod_id] = object
|
261
330
|
|
262
|
-
|
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
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
291
|
-
|
292
|
-
|
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
|
-
|
298
|
-
|
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
|
-
|
314
|
-
|
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
|
406
|
-
meta = super
|
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+
|
679
|
-
def self.path_for_index(relative_path,field
|
680
|
-
|
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(
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
+
|