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