rod 0.6.0
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/Gemfile +2 -0
- data/README +144 -0
- data/Rakefile +58 -0
- data/changelog.txt +99 -0
- data/lib/rod.rb +21 -0
- data/lib/rod/abstract_database.rb +369 -0
- data/lib/rod/collection_proxy.rb +72 -0
- data/lib/rod/constants.rb +32 -0
- data/lib/rod/database.rb +598 -0
- data/lib/rod/exception.rb +56 -0
- data/lib/rod/join_element.rb +63 -0
- data/lib/rod/model.rb +926 -0
- data/lib/rod/segmented_index.rb +85 -0
- data/lib/rod/string_element.rb +37 -0
- data/lib/rod/string_ex.rb +14 -0
- data/rod.gemspec +28 -0
- metadata +167 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
# Exceptions defined by the library.
|
2
|
+
module Rod
|
3
|
+
# Base class for all Rod exceptions
|
4
|
+
class RodException < Exception
|
5
|
+
def initialize(message)
|
6
|
+
@message = message
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
"Rod exception: #{@message}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# This exceptions is raised if there is a validation error.
|
15
|
+
class ValidationException < RodException
|
16
|
+
def initialize(message)
|
17
|
+
@message = message
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
@message.join("\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Base exception class for database errors
|
26
|
+
class DatabaseError < RodException
|
27
|
+
def to_s
|
28
|
+
"Database error: #{@message}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# This exception is raised if there is no database linked with the class.
|
33
|
+
class MissingDatabase < DatabaseError
|
34
|
+
def initialize(klass)
|
35
|
+
@klass = klass
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
"Database not selected for class #{@klass}!\n" +
|
40
|
+
"Provide the database class via call to Rod::Model.database_class."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# This exception is raised if argument for some Rod API call
|
45
|
+
# (such as field, has_one, has_many) is invalid.
|
46
|
+
class InvalidArgument < RodException
|
47
|
+
def initialize(value,type)
|
48
|
+
@value = value
|
49
|
+
@type = type
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
"The value '#@value' of the #@type is invalid!"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Rod
|
2
|
+
class JoinElement
|
3
|
+
def self.typedef_struct
|
4
|
+
str = <<-END
|
5
|
+
|typedef struct {
|
6
|
+
| unsigned long offset;
|
7
|
+
| unsigned long index;
|
8
|
+
|} _join_element;
|
9
|
+
END
|
10
|
+
str.margin
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.layout
|
14
|
+
' printf(" offset: %lu, index: %lu\n",sizeof(unsigned long), sizeof(unsigned long));' + "\n"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.struct_name
|
18
|
+
"_join_element"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.path_for_data(path)
|
22
|
+
"#{path}#{self.struct_name}.dat"
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.page_offsets
|
26
|
+
@page_offsets ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.fields
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.build_structure
|
34
|
+
# does nothing, the structure is not needed
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.cache
|
38
|
+
@cache ||= SimpleWeakHash.new
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class PolymorphicJoinElement < JoinElement
|
43
|
+
def self.typedef_struct
|
44
|
+
str = <<-END
|
45
|
+
|typedef struct {
|
46
|
+
| unsigned long offset;
|
47
|
+
| unsigned long index;
|
48
|
+
| unsigned long class;
|
49
|
+
|} _polymorphic_join_element;
|
50
|
+
END
|
51
|
+
str.margin
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.struct_name
|
55
|
+
"_polymorphic_join_element"
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.layout
|
59
|
+
' printf(" offset: %lu, index: %lu, class: %lu\n",' +
|
60
|
+
'sizeof(unsigned long), sizeof(unsigned long), sizeof(unsigned long));' + "\n"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/rod/model.rb
ADDED
@@ -0,0 +1,926 @@
|
|
1
|
+
require 'rod/constants'
|
2
|
+
require 'rod/collection_proxy'
|
3
|
+
|
4
|
+
module Rod
|
5
|
+
|
6
|
+
# Abstract class representing a model entity. Each storable class has to derieve from +Model+.
|
7
|
+
class Model
|
8
|
+
include ActiveModel::Validations
|
9
|
+
extend Enumerable
|
10
|
+
|
11
|
+
# If +options+ is an integer it is the @rod_id of the object.
|
12
|
+
def initialize(options=nil)
|
13
|
+
case options
|
14
|
+
when Integer
|
15
|
+
@rod_id = options
|
16
|
+
when Hash
|
17
|
+
options.each do |key,value|
|
18
|
+
begin
|
19
|
+
self.send("#{key}=",value)
|
20
|
+
rescue NoMethodError
|
21
|
+
raise RodException.new("There is no field or association with name #{key}!")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
@rod_id = 0
|
25
|
+
else
|
26
|
+
@rod_id = 0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#########################################################################
|
31
|
+
# Public API
|
32
|
+
#########################################################################
|
33
|
+
|
34
|
+
# Stores the instance in the database. This might be called
|
35
|
+
# only if the database is opened for writing (see +create+).
|
36
|
+
# To skip validation pass false.
|
37
|
+
def store(validate=true)
|
38
|
+
if validate
|
39
|
+
if valid?
|
40
|
+
self.class.store(self)
|
41
|
+
else
|
42
|
+
raise ValidationException.new([self.to_s,self.errors.full_messages])
|
43
|
+
end
|
44
|
+
else
|
45
|
+
self.class.store(self)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Default implementation of equality.
|
50
|
+
def ==(other)
|
51
|
+
self.class == other.class && self.rod_id == other.rod_id
|
52
|
+
end
|
53
|
+
|
54
|
+
# Default implementation of to_s.
|
55
|
+
def to_s
|
56
|
+
fields = self.class.fields.map{|n,o| "#{n}:#{self.send(n)}"}.join(",")
|
57
|
+
singular = self.class.singular_associations.map{|n,o| "#{n}:#{self.send(n).class}"}.join(",")
|
58
|
+
plural = self.class.plural_associations.map{|n,o| "#{n}:#{self.send(n).size}"}.join(",")
|
59
|
+
"#{self.class}:<#{fields}><#{singular}><#{plural}>"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the number of objects of this class stored in the
|
63
|
+
# database.
|
64
|
+
def self.count
|
65
|
+
self_count = database.count(self)
|
66
|
+
# This should be changed if all other featurs connected with
|
67
|
+
# inheritence are implemented, especially #14
|
68
|
+
#subclasses.inject(self_count){|sum,sub| sum + sub.count}
|
69
|
+
self_count
|
70
|
+
end
|
71
|
+
|
72
|
+
# Iterates over object of this class stored in the database.
|
73
|
+
def self.each
|
74
|
+
#TODO an exception if in wrong state?
|
75
|
+
if block_given?
|
76
|
+
self.count.times do |index|
|
77
|
+
yield get(index+1)
|
78
|
+
end
|
79
|
+
else
|
80
|
+
enum_for(:each)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns n-th (+index+) object of this class stored in the database.
|
85
|
+
# This call is scope-checked.
|
86
|
+
def self.[](index)
|
87
|
+
if index >= 0 && index < self.count
|
88
|
+
get(index+1)
|
89
|
+
else
|
90
|
+
raise IndexError.
|
91
|
+
new("The index #{index} is out of the scope [0...#{self.count}] for #{self}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
# A macro-style function used to indicate that given piece of data
|
97
|
+
# is stored in the database.
|
98
|
+
# Type should be one of:
|
99
|
+
# * +:integer+
|
100
|
+
# * +:ulong+
|
101
|
+
# * +:float+
|
102
|
+
# * +:string+
|
103
|
+
# * +:object+ (value is marshaled durign storage, and unmarshaled during read)
|
104
|
+
# Options:
|
105
|
+
# * +:index+ builds an index for the field and might be:
|
106
|
+
# ** +:flat+ simple hash index (+true+ works as well for backwards compatiblity)
|
107
|
+
# ** +:segmented+ index split for 1001 pieces for shorter load times (only
|
108
|
+
# one piece is loaded on one look-up)
|
109
|
+
#
|
110
|
+
# Warning!
|
111
|
+
# rod_id is a predefined field
|
112
|
+
def self.field(name, type, options={})
|
113
|
+
ensure_valid_name(name)
|
114
|
+
ensure_valid_type(type)
|
115
|
+
self.fields[name] = options.merge({:type => type})
|
116
|
+
end
|
117
|
+
|
118
|
+
# A macro-style function used to indicate that instances of this
|
119
|
+
# class are associated with many instances of some other class. The
|
120
|
+
# name of the class is guessed from the field name, but you can
|
121
|
+
# change it via options.
|
122
|
+
# Options:
|
123
|
+
# * +:class_name+ - the name of the class (as String) associated
|
124
|
+
# with this class
|
125
|
+
# * +:polymorphic+ - if set to +true+ the association is polymorphic (allows to acess
|
126
|
+
# objects of different classes via this association)
|
127
|
+
def self.has_many(name, options={})
|
128
|
+
ensure_valid_name(name)
|
129
|
+
self.plural_associations[name] = options
|
130
|
+
end
|
131
|
+
|
132
|
+
# A macro-style function used to indicate that instances of this
|
133
|
+
# class are associated with one instance of some other class. The
|
134
|
+
# name of the class is guessed from the field name, but you can
|
135
|
+
# change it via options.
|
136
|
+
# Options:
|
137
|
+
# * +:class_name+ - the name of the class (as String) associated
|
138
|
+
# with this class
|
139
|
+
# * +:polymorphic+ - if set to +true+ the association is polymorphic (allows to acess
|
140
|
+
# objects of different classes via this association)
|
141
|
+
def self.has_one(name, options={})
|
142
|
+
ensure_valid_name(name)
|
143
|
+
self.singular_associations[name] = options
|
144
|
+
end
|
145
|
+
|
146
|
+
# A macro-style function used to link the model with specific
|
147
|
+
# database class. See notes on Rod::Database for further
|
148
|
+
# information why this is needed.
|
149
|
+
def self.database_class(klass)
|
150
|
+
unless @database.nil?
|
151
|
+
@database.remove_class(self)
|
152
|
+
end
|
153
|
+
@database = klass.instance
|
154
|
+
self.add_to_database
|
155
|
+
end
|
156
|
+
|
157
|
+
#########################################################################
|
158
|
+
# 'Private' instance methods
|
159
|
+
#########################################################################
|
160
|
+
|
161
|
+
public
|
162
|
+
# Update the DB information about the +object+ which
|
163
|
+
# is referenced via singular association with +name+.
|
164
|
+
def update_singular_association(name, object)
|
165
|
+
rod_id = object.nil? ? 0 : object.rod_id
|
166
|
+
send("_#{name}=", @rod_id, rod_id)
|
167
|
+
if self.class.singular_associations[name][:polymorphic]
|
168
|
+
class_id = object.nil? ? 0 : object.class.name_hash
|
169
|
+
send("_#{name}__class=", @rod_id, class_id)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Update in the DB information about the +object+ (or objects) which is (are)
|
174
|
+
# referenced via plural association with +name+.
|
175
|
+
#
|
176
|
+
# The name of the association is +name+, the referenced
|
177
|
+
# object(s) is (are) +object+.
|
178
|
+
# +index+ is the position of the referenced object in the association.
|
179
|
+
# If there are many objects, the index is ignored.
|
180
|
+
def update_plural_association(name, object, index=nil)
|
181
|
+
offset = send("_#{name}_offset",@rod_id)
|
182
|
+
if self.class.plural_associations[name][:polymorphic]
|
183
|
+
# If you wish to refactor this code, ensure performance is preserved.
|
184
|
+
if object.respond_to?(:each)
|
185
|
+
objects = object
|
186
|
+
objects.each.with_index do |object,index|
|
187
|
+
rod_id = object.nil? ? 0 : object.rod_id
|
188
|
+
class_id = object.nil? ? 0 : object.class.name_hash
|
189
|
+
database.set_polymorphic_join_element_id(offset, index, rod_id,
|
190
|
+
class_id)
|
191
|
+
end
|
192
|
+
else
|
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
|
+
# If you wish to refactor this code, ensure performance is preserved.
|
200
|
+
if object.respond_to?(:each)
|
201
|
+
objects = object
|
202
|
+
objects.each.with_index do |object,index|
|
203
|
+
rod_id = object.nil? ? 0 : object.rod_id
|
204
|
+
database.set_join_element_id(offset, index, rod_id)
|
205
|
+
end
|
206
|
+
else
|
207
|
+
rod_id = object.nil? ? 0 : object.rod_id
|
208
|
+
database.set_join_element_id(offset, index, rod_id)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Updates in the DB the +count+ and +offset+ of elements for +name+ association.
|
214
|
+
def update_count_and_offset(name,count,offset)
|
215
|
+
send("_#{name}_count=",@rod_id,count)
|
216
|
+
send("_#{name}_offset=",@rod_id,offset)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Updates in the DB the field +name+ to the actual value.
|
220
|
+
def update_field(name)
|
221
|
+
if self.class.string_field?(self.class.fields[name][:type])
|
222
|
+
if self.class.fields[name][:type] == :string
|
223
|
+
value = send(name)
|
224
|
+
elsif self.class.fields[name][:type] == :object
|
225
|
+
value = instance_variable_get("@#{name}")
|
226
|
+
value = Marshal.dump(value)
|
227
|
+
else
|
228
|
+
raise RodException.new("Unrecognised field type '#{self.class.fields[name][:type]}'!")
|
229
|
+
end
|
230
|
+
length, offset = database.set_string(value)
|
231
|
+
send("_#{name}_length=",@rod_id,length)
|
232
|
+
send("_#{name}_offset=",@rod_id,offset)
|
233
|
+
else
|
234
|
+
send("_#{name}=",@rod_id,send(name))
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
#########################################################################
|
239
|
+
# 'Private' class methods
|
240
|
+
#########################################################################
|
241
|
+
|
242
|
+
# Stores given +object+ in the database. The object must be an
|
243
|
+
# instance of this class.
|
244
|
+
def self.store(object)
|
245
|
+
unless object.is_a?(self)
|
246
|
+
raise RodException.new("Incompatible object class #{object.class}.")
|
247
|
+
end
|
248
|
+
unless object.rod_id == 0
|
249
|
+
raise RodException.new("The object #{object} is allready stored!")
|
250
|
+
end
|
251
|
+
database.store(self,object)
|
252
|
+
|
253
|
+
# update indices
|
254
|
+
properties.each do |property,options|
|
255
|
+
if options[:index]
|
256
|
+
keys =
|
257
|
+
if field?(property)
|
258
|
+
[object.send(property)]
|
259
|
+
elsif singular_association?(property)
|
260
|
+
[object.send(property).rod_id]
|
261
|
+
else
|
262
|
+
object.send(property).map{|o| o.rod_id}
|
263
|
+
end
|
264
|
+
keys.each do |key|
|
265
|
+
proxy = self.index_for(property,options,key)
|
266
|
+
if proxy.nil?
|
267
|
+
proxy = self.set_values_for(property,options,key,0) do |index|
|
268
|
+
raise RodException.new("Calling fetch block for an empty proxy!")
|
269
|
+
end
|
270
|
+
else
|
271
|
+
unless proxy.is_a?(CollectionProxy)
|
272
|
+
offset, count = proxy
|
273
|
+
proxy = self.set_values_for(property,options,key,count) do |index|
|
274
|
+
[database.join_index(offset,index), self]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
proxy << [object.rod_id,object.class]
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# update object that references the stored object
|
284
|
+
referenced_objects ||= database.referenced_objects
|
285
|
+
# ... via singular associations
|
286
|
+
singular_associations.each do |name, options|
|
287
|
+
referenced = object.send(name)
|
288
|
+
unless referenced.nil?
|
289
|
+
# There is a referenced object, but its rod_id is not set.
|
290
|
+
if referenced.rod_id == 0
|
291
|
+
unless referenced_objects.has_key?(referenced)
|
292
|
+
referenced_objects[referenced] = []
|
293
|
+
end
|
294
|
+
referenced_objects[referenced].push([object.rod_id, name,
|
295
|
+
object.class.name_hash])
|
296
|
+
end
|
297
|
+
# clear references, allowing for garbage collection
|
298
|
+
object.send("#{name}=",nil)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# ... via plural associations
|
303
|
+
plural_associations.each do |name, options|
|
304
|
+
referenced = object.send(name)
|
305
|
+
unless referenced.nil?
|
306
|
+
referenced.each_with_index do |element, index|
|
307
|
+
# There are referenced objects, but their rod_id is not set
|
308
|
+
if !element.nil? && element.rod_id == 0
|
309
|
+
unless referenced_objects.has_key?(element)
|
310
|
+
referenced_objects[element] = []
|
311
|
+
end
|
312
|
+
referenced_objects[element].push([object.rod_id, name,
|
313
|
+
object.class.name_hash, index])
|
314
|
+
end
|
315
|
+
end
|
316
|
+
# clear references, allowing for garbage collection
|
317
|
+
object.send("#{name}=",nil)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
reverse_references = referenced_objects.delete(object)
|
322
|
+
|
323
|
+
unless reverse_references.blank?
|
324
|
+
reverse_references.each do |referee_rod_id, method_name, class_id, index|
|
325
|
+
referee = Model.get_class(class_id).find_by_rod_id(referee_rod_id)
|
326
|
+
self.cache.send(:__get_hash__).delete(referee_rod_id)
|
327
|
+
if index.nil?
|
328
|
+
# singular association
|
329
|
+
referee.update_singular_association(method_name, object)
|
330
|
+
else
|
331
|
+
referee.update_plural_association(method_name, object, index)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# The name of the C struct for this class.
|
338
|
+
def self.struct_name
|
339
|
+
return @struct_name unless @struct_name.nil?
|
340
|
+
name = self.to_s.underscore.gsub(/\//,"__")
|
341
|
+
unless name =~ /^\#/
|
342
|
+
# not an anonymous class
|
343
|
+
@struct_name = name
|
344
|
+
end
|
345
|
+
name
|
346
|
+
end
|
347
|
+
|
348
|
+
# Finder for rod_id.
|
349
|
+
def self.find_by_rod_id(rod_id)
|
350
|
+
if rod_id <= 0 || rod_id > self.count
|
351
|
+
return nil
|
352
|
+
end
|
353
|
+
get(rod_id)
|
354
|
+
end
|
355
|
+
|
356
|
+
# Returns the fields of this class.
|
357
|
+
def self.fields
|
358
|
+
if self == Rod::Model
|
359
|
+
@fields ||= {"rod_id" => {:type => :ulong}}
|
360
|
+
else
|
361
|
+
@fields ||= superclass.fields.dup
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
# Returns singular associations of this class.
|
366
|
+
def self.singular_associations
|
367
|
+
if self == Rod::Model
|
368
|
+
@singular_associations ||= {}
|
369
|
+
else
|
370
|
+
@singular_associations ||= superclass.singular_associations.dup
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Returns plural associations of this class.
|
375
|
+
def self.plural_associations
|
376
|
+
if self == Rod::Model
|
377
|
+
@plural_associations ||= {}
|
378
|
+
else
|
379
|
+
@plural_associations ||= superclass.plural_associations.dup
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
protected
|
384
|
+
# The pointer to the mmaped table of C structs.
|
385
|
+
def self.rod_pointer
|
386
|
+
@rod_pointer
|
387
|
+
end
|
388
|
+
|
389
|
+
# Writer for the pointer to the mmaped table of C structs.
|
390
|
+
def self.rod_pointer=(value)
|
391
|
+
@rod_pointer = value
|
392
|
+
end
|
393
|
+
|
394
|
+
# Used for establishing link with the DB.
|
395
|
+
def self.inherited(subclass)
|
396
|
+
subclass.add_to_class_space
|
397
|
+
subclasses << subclass
|
398
|
+
begin
|
399
|
+
subclass.add_to_database
|
400
|
+
rescue MissingDatabase
|
401
|
+
# This might happen for classes which inherit directly from
|
402
|
+
# the Rod::Model. Since the +inherited+ method is always called
|
403
|
+
# before the +database_class+ call, they never have the DB set-up
|
404
|
+
# when this is called.
|
405
|
+
# +add_to_database+ is called within +database_class+ for them.
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# Returns the subclasses of this class
|
410
|
+
def self.subclasses
|
411
|
+
@subclasses ||= []
|
412
|
+
@subclasses
|
413
|
+
end
|
414
|
+
|
415
|
+
# Add self to the database it is linked to.
|
416
|
+
def self.add_to_database
|
417
|
+
self.database.add_class(self)
|
418
|
+
end
|
419
|
+
|
420
|
+
# Add self to the Rod model class space. This is need
|
421
|
+
# to determine the class for polymorphic associations.
|
422
|
+
def self.add_to_class_space
|
423
|
+
Model.add_class(self)
|
424
|
+
end
|
425
|
+
|
426
|
+
# Adds given +klass+ to the class space.
|
427
|
+
# This method is used only for Model class itself. It should
|
428
|
+
# not be called for the subclasses.
|
429
|
+
def self.add_class(klass)
|
430
|
+
raise RodException.new("'add_class' method is final for Rod::Model") if self != Model
|
431
|
+
@class_space ||= {}
|
432
|
+
@class_space[klass.name_hash] = klass
|
433
|
+
end
|
434
|
+
|
435
|
+
def self.get_class(klass_hash)
|
436
|
+
raise RodException.new("'get_class' method is final for Rod::Model") if self != Model
|
437
|
+
@class_space ||= {}
|
438
|
+
klass = @class_space[klass_hash]
|
439
|
+
if klass.nil?
|
440
|
+
raise RodException.new("There is no class with name hash '#{klass_hash}'!\n" +
|
441
|
+
"Check if all needed classes are loaded.")
|
442
|
+
end
|
443
|
+
klass
|
444
|
+
end
|
445
|
+
|
446
|
+
# Returns the database given instance belongs to (is or will be stored within).
|
447
|
+
def database
|
448
|
+
self.class.database
|
449
|
+
end
|
450
|
+
|
451
|
+
# Checks if the name of the field or association is valid.
|
452
|
+
def self.ensure_valid_name(name)
|
453
|
+
if name.to_s.empty? || INVALID_NAMES.has_key?(name)
|
454
|
+
raise InvalidArgument.new(name,"field/association name")
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# Checks if the type of the field is valid.
|
459
|
+
def self.ensure_valid_type(type)
|
460
|
+
unless TYPE_MAPPING.has_key?(type)
|
461
|
+
raise InvalidArgument.new(type,"field type")
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
# Returns the database this class is linked to.
|
466
|
+
# The database class is configured with the call to
|
467
|
+
# macro-style function +database_class+. This information
|
468
|
+
# is inherited, so it have to be defined only for the
|
469
|
+
# root-class of the model (if such a class exists).
|
470
|
+
def self.database
|
471
|
+
return @database unless @database.nil?
|
472
|
+
if self.superclass.respond_to?(:database)
|
473
|
+
@database = self.superclass.database
|
474
|
+
else
|
475
|
+
raise MissingDatabase.new(self)
|
476
|
+
end
|
477
|
+
@database
|
478
|
+
end
|
479
|
+
|
480
|
+
# The object cache of this class.
|
481
|
+
# XXX consider moving it to the database.
|
482
|
+
def self.cache
|
483
|
+
@cache ||= SimpleWeakHash.new
|
484
|
+
end
|
485
|
+
|
486
|
+
# The module context of the class.
|
487
|
+
def self.module_context
|
488
|
+
context = name[0...(name.rindex( '::' ) || 0)]
|
489
|
+
context.empty? ? Object : eval(context)
|
490
|
+
end
|
491
|
+
|
492
|
+
# Returns the name of the scope of the class.
|
493
|
+
def self.scope_name
|
494
|
+
if self.module_context == Object
|
495
|
+
""
|
496
|
+
else
|
497
|
+
self.module_context.to_s
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
#########################################################################
|
502
|
+
# DB-oriented API
|
503
|
+
#########################################################################
|
504
|
+
|
505
|
+
# The SHA2 digest of the class name
|
506
|
+
#
|
507
|
+
# Warning: if you dynamically create classes (via Class.new)
|
508
|
+
# this value is random, until the class is bound with a constant!
|
509
|
+
def self.name_hash
|
510
|
+
return @name_hash unless @name_hash.nil?
|
511
|
+
# This is not used to protect any value, only to
|
512
|
+
# distinguish names of classes. It doesn't have to be
|
513
|
+
# very strong agains collision attacks.
|
514
|
+
@name_hash = Digest::SHA2.new.hexdigest(self.struct_name).
|
515
|
+
to_s.to_i(16) % 2 ** 32
|
516
|
+
end
|
517
|
+
|
518
|
+
# The name of the file (for given +relative_path+), which the data of this class
|
519
|
+
# is stored in.
|
520
|
+
def self.path_for_data(relative_path)
|
521
|
+
"#{relative_path}#{self.struct_name}.dat"
|
522
|
+
end
|
523
|
+
|
524
|
+
# The name of the file or directory (for given +relative_path+), which the
|
525
|
+
# index of the +field+ (with +options+) of this class is stored in.
|
526
|
+
def self.path_for_index(relative_path,field,options)
|
527
|
+
case options[:index]
|
528
|
+
when :flat,true
|
529
|
+
"#{relative_path}#{self.struct_name}_#{field}.idx"
|
530
|
+
when :segmented
|
531
|
+
"#{relative_path}#{self.struct_name}_#{field}_idx/"
|
532
|
+
else
|
533
|
+
raise RodException.new("Invalid index type #{type}")
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
# Returns true if the type of the filed is string-like (i.e. stored as
|
538
|
+
# StringElement).
|
539
|
+
def self.string_field?(type)
|
540
|
+
string_types.include?(type)
|
541
|
+
end
|
542
|
+
|
543
|
+
# Types which are stored as strings.
|
544
|
+
def self.string_types
|
545
|
+
[:string, :object]
|
546
|
+
end
|
547
|
+
|
548
|
+
# The C structure representing this class.
|
549
|
+
def self.typedef_struct
|
550
|
+
result = <<-END
|
551
|
+
|typedef struct {
|
552
|
+
| \n#{self.fields.map do |field,options|
|
553
|
+
unless string_field?(options[:type])
|
554
|
+
"| #{TYPE_MAPPING[options[:type]]} #{field};"
|
555
|
+
else
|
556
|
+
<<-SUBEND
|
557
|
+
| unsigned long #{field}_length;
|
558
|
+
| unsigned long #{field}_offset;
|
559
|
+
SUBEND
|
560
|
+
end
|
561
|
+
end.join("\n| \n") }
|
562
|
+
| #{singular_associations.map do |name, options|
|
563
|
+
result = "unsigned long #{name};"
|
564
|
+
if options[:polymorphic]
|
565
|
+
result += " unsigned long #{name}__class;"
|
566
|
+
end
|
567
|
+
result
|
568
|
+
end.join("\n| ")}
|
569
|
+
| \n#{plural_associations.map do |name, options|
|
570
|
+
result =
|
571
|
+
"| unsigned long #{name}_offset;\n"+
|
572
|
+
"| unsigned long #{name}_count;"
|
573
|
+
result
|
574
|
+
end.join("\n| \n")}
|
575
|
+
|} #{struct_name()};
|
576
|
+
END
|
577
|
+
result.margin
|
578
|
+
end
|
579
|
+
|
580
|
+
# Prints the memory layout of the structure.
|
581
|
+
def self.layout
|
582
|
+
result = <<-END
|
583
|
+
| \n#{self.fields.map do |field,options|
|
584
|
+
unless string_field?(options[:type])
|
585
|
+
"| printf(\" size of '#{field}': %lu\\n\",sizeof(#{TYPE_MAPPING[options[:type]]}));"
|
586
|
+
else
|
587
|
+
<<-SUBEND
|
588
|
+
| printf(" string '#{field}' length: %lu offset: %lu page: %lu\\n",
|
589
|
+
| sizeof(unsigned long), sizeof(unsigned long), sizeof(unsigned long));
|
590
|
+
SUBEND
|
591
|
+
end
|
592
|
+
end.join("\n") }
|
593
|
+
| \n#{singular_associations.map do |name, options|
|
594
|
+
" printf(\" singular assoc '#{name}': %lu\\n\",sizeof(unsigned long));"
|
595
|
+
end.join("\n| ")}
|
596
|
+
| \n#{plural_associations.map do |name, options|
|
597
|
+
"| printf(\" plural assoc '#{name}' offset: %lu, count %lu\\n\",\n"+
|
598
|
+
"| sizeof(unsigned long),sizeof(unsigned long));"
|
599
|
+
end.join("\n| \n")}
|
600
|
+
END
|
601
|
+
result.margin
|
602
|
+
end
|
603
|
+
|
604
|
+
# Reads the value of a specified field of the C-structure.
|
605
|
+
def self.field_reader(name,result_type,builder)
|
606
|
+
str =<<-END
|
607
|
+
|#{result_type} _#{name}(unsigned long object_rod_id){
|
608
|
+
| VALUE klass = rb_funcall(self,rb_intern("class"),0);
|
609
|
+
| #{struct_name} * pointer = (#{struct_name} *)
|
610
|
+
| NUM2ULONG(rb_funcall(klass,rb_intern("rod_pointer"),0));
|
611
|
+
| return (pointer + object_rod_id - 1)->#{name};
|
612
|
+
|}
|
613
|
+
END
|
614
|
+
builder.c(str.margin)
|
615
|
+
end
|
616
|
+
|
617
|
+
# Writes the value of a specified field of the C-structure.
|
618
|
+
def self.field_writer(name,arg_type,builder)
|
619
|
+
str =<<-END
|
620
|
+
|void _#{name}_equals(unsigned long object_rod_id,#{arg_type} value){
|
621
|
+
| VALUE klass = rb_funcall(self,rb_intern("class"),0);
|
622
|
+
| #{struct_name} * pointer = (#{struct_name} *)
|
623
|
+
| NUM2ULONG(rb_funcall(klass,rb_intern("rod_pointer"),0));
|
624
|
+
| (pointer + object_rod_id - 1)->#{name} = value;
|
625
|
+
|}
|
626
|
+
END
|
627
|
+
builder.c(str.margin)
|
628
|
+
end
|
629
|
+
|
630
|
+
#########################################################################
|
631
|
+
# Generated methods
|
632
|
+
#########################################################################
|
633
|
+
|
634
|
+
# This code intializes the class. It adds C routines and dynamic Ruby accessors.
|
635
|
+
def self.build_structure
|
636
|
+
self.fields.each do |name, options|
|
637
|
+
if options[:index]
|
638
|
+
instance_variable_set("@#{name}_index",nil)
|
639
|
+
end
|
640
|
+
end
|
641
|
+
return if @structure_built
|
642
|
+
|
643
|
+
inline(:C) do |builder|
|
644
|
+
builder.prefix(typedef_struct)
|
645
|
+
if Database.development_mode
|
646
|
+
# This method is created to force rebuild of the C code, since
|
647
|
+
# it is rebuild on the basis of methods' signatures change.
|
648
|
+
builder.c_singleton("void __unused_method_#{rand(1000)}(){}")
|
649
|
+
end
|
650
|
+
|
651
|
+
self.fields.each do |name, options|
|
652
|
+
unless string_field?(options[:type])
|
653
|
+
field_reader(name,TYPE_MAPPING[options[:type]],builder)
|
654
|
+
field_writer(name,TYPE_MAPPING[options[:type]],builder)
|
655
|
+
else
|
656
|
+
field_reader("#{name}_length","unsigned long",builder)
|
657
|
+
field_reader("#{name}_offset","unsigned long",builder)
|
658
|
+
field_writer("#{name}_length","unsigned long",builder)
|
659
|
+
field_writer("#{name}_offset","unsigned long",builder)
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
singular_associations.each do |name, options|
|
664
|
+
field_reader(name,"unsigned long",builder)
|
665
|
+
field_writer(name,"unsigned long",builder)
|
666
|
+
if options[:polymorphic]
|
667
|
+
field_reader("#{name}__class","unsigned long",builder)
|
668
|
+
field_writer("#{name}__class","unsigned long",builder)
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
plural_associations.each do |name, options|
|
673
|
+
field_reader("#{name}_count","unsigned long",builder)
|
674
|
+
field_reader("#{name}_offset","unsigned long",builder)
|
675
|
+
field_writer("#{name}_count","unsigned long",builder)
|
676
|
+
field_writer("#{name}_offset","unsigned long",builder)
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
## accessors for fields, plural and singular relationships follow
|
681
|
+
self.fields.each do |field, options|
|
682
|
+
# optimization
|
683
|
+
field = field.to_s
|
684
|
+
# adding new private fields visible from Ruby
|
685
|
+
# they are lazily initialized based on the C representation
|
686
|
+
unless string_field?(options[:type])
|
687
|
+
private "_#{field}", "_#{field}="
|
688
|
+
else
|
689
|
+
private "_#{field}_length", "_#{field}_offset"
|
690
|
+
end
|
691
|
+
|
692
|
+
unless string_field?(options[:type])
|
693
|
+
# getter
|
694
|
+
define_method(field) do
|
695
|
+
value = instance_variable_get("@#{field}")
|
696
|
+
if value.nil?
|
697
|
+
if @rod_id == 0
|
698
|
+
value = nil
|
699
|
+
else
|
700
|
+
value = send("_#{field}",@rod_id)
|
701
|
+
end
|
702
|
+
instance_variable_set("@#{field}",value)
|
703
|
+
end
|
704
|
+
value
|
705
|
+
end
|
706
|
+
|
707
|
+
# setter
|
708
|
+
define_method("#{field}=") do |value|
|
709
|
+
instance_variable_set("@#{field}",value)
|
710
|
+
value
|
711
|
+
end
|
712
|
+
else
|
713
|
+
# string-type fields
|
714
|
+
# getter
|
715
|
+
define_method(field) do
|
716
|
+
value = instance_variable_get("@#{field}")
|
717
|
+
if value.nil? # first call
|
718
|
+
if @rod_id == 0
|
719
|
+
return (options[:type] == :object ? nil : "")
|
720
|
+
else
|
721
|
+
length = send("_#{field}_length", @rod_id)
|
722
|
+
if length == 0
|
723
|
+
return (options[:type] == :object ? nil : "")
|
724
|
+
end
|
725
|
+
offset = send("_#{field}_offset", @rod_id)
|
726
|
+
value = database.read_string(length, offset)
|
727
|
+
if options[:type] == :object
|
728
|
+
value = Marshal.load(value)
|
729
|
+
end
|
730
|
+
# caching Ruby representation
|
731
|
+
send("#{field}=",value)
|
732
|
+
end
|
733
|
+
end
|
734
|
+
value
|
735
|
+
end
|
736
|
+
|
737
|
+
# setter
|
738
|
+
define_method("#{field}=") do |value|
|
739
|
+
instance_variable_set("@#{field}",value)
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
end
|
744
|
+
|
745
|
+
singular_associations.each do |name, options|
|
746
|
+
# optimization
|
747
|
+
name = name.to_s
|
748
|
+
private "_#{name}", "_#{name}="
|
749
|
+
class_name =
|
750
|
+
if options[:class_name]
|
751
|
+
options[:class_name]
|
752
|
+
else
|
753
|
+
"#{self.scope_name}::#{name.camelcase}"
|
754
|
+
end
|
755
|
+
|
756
|
+
#getter
|
757
|
+
define_method(name) do
|
758
|
+
value = instance_variable_get("@#{name}")
|
759
|
+
if value.nil?
|
760
|
+
rod_id = send("_#{name}",@rod_id)
|
761
|
+
# the indices are shifted by 1, to leave 0 for nil
|
762
|
+
if rod_id == 0
|
763
|
+
value = nil
|
764
|
+
else
|
765
|
+
if options[:polymorphic]
|
766
|
+
klass = Model.get_class(send("_#{name}__class",@rod_id))
|
767
|
+
value = klass.find_by_rod_id(rod_id)
|
768
|
+
else
|
769
|
+
value = class_name.constantize.find_by_rod_id(rod_id)
|
770
|
+
end
|
771
|
+
end
|
772
|
+
send("#{name}=",value)
|
773
|
+
end
|
774
|
+
value
|
775
|
+
end
|
776
|
+
|
777
|
+
#setter
|
778
|
+
define_method("#{name}=") do |value|
|
779
|
+
instance_variable_set("@#{name}", value)
|
780
|
+
end
|
781
|
+
end
|
782
|
+
|
783
|
+
plural_associations.each do |name, options|
|
784
|
+
# optimization
|
785
|
+
name = name.to_s
|
786
|
+
class_name =
|
787
|
+
if options[:class_name]
|
788
|
+
options[:class_name]
|
789
|
+
else
|
790
|
+
"#{self.scope_name}::#{::English::Inflect.singular(name).camelcase}"
|
791
|
+
end
|
792
|
+
klass = class_name.constantize unless options[:polymorphic]
|
793
|
+
|
794
|
+
# getter
|
795
|
+
define_method("#{name}") do
|
796
|
+
proxy = instance_variable_get("@#{name}")
|
797
|
+
if proxy.nil?
|
798
|
+
if @rod_id == 0
|
799
|
+
count = 0
|
800
|
+
else
|
801
|
+
count = self.send("_#{name}_count",@rod_id)
|
802
|
+
end
|
803
|
+
return instance_variable_set("@#{name}",[]) if count == 0
|
804
|
+
offset = self.send("_#{name}_offset",@rod_id)
|
805
|
+
unless options[:polymorphic]
|
806
|
+
proxy = CollectionProxy.new(count) do |index|
|
807
|
+
[database.join_index(offset,index), klass]
|
808
|
+
end
|
809
|
+
else
|
810
|
+
proxy = CollectionProxy.new(count) do |index|
|
811
|
+
rod_id = database.polymorphic_join_index(offset,index)
|
812
|
+
class_id = database.polymorphic_join_class(offset,index)
|
813
|
+
[rod_id, rod_id == 0 ? 0 : Model.get_class(class_id)]
|
814
|
+
end
|
815
|
+
end
|
816
|
+
instance_variable_set("@#{name}", proxy)
|
817
|
+
end
|
818
|
+
proxy
|
819
|
+
end
|
820
|
+
|
821
|
+
# count getter
|
822
|
+
define_method("#{name}_count") do
|
823
|
+
if (instance_variable_get("@#{name}") != nil)
|
824
|
+
return instance_variable_get("@#{name}").count
|
825
|
+
else
|
826
|
+
return send("_#{name}_count",@rod_id)
|
827
|
+
end
|
828
|
+
end
|
829
|
+
|
830
|
+
# setter
|
831
|
+
define_method("#{name}=") do |value|
|
832
|
+
instance_variable_set("@#{name}", value)
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
# indices
|
837
|
+
properties.each do |property,options|
|
838
|
+
# optimization
|
839
|
+
property = property.to_s
|
840
|
+
if options[:index]
|
841
|
+
(class << self; self; end).class_eval do
|
842
|
+
# Find all objects with given +value+ of the +property+.
|
843
|
+
define_method("find_all_by_#{property}") do |value|
|
844
|
+
value = value.rod_id if value.is_a?(Model)
|
845
|
+
offset,count = index_for(property,options,value)
|
846
|
+
return [] if offset.nil?
|
847
|
+
CollectionProxy.new(count) do |index|
|
848
|
+
[database.join_index(offset,index),self]
|
849
|
+
end
|
850
|
+
end
|
851
|
+
|
852
|
+
# Find first object with given +value+ of the +property+.
|
853
|
+
define_method("find_by_#{property}") do |value|
|
854
|
+
offset,count = index_for(property,options)[value]
|
855
|
+
if offset.nil?
|
856
|
+
nil
|
857
|
+
else
|
858
|
+
get(database.join_index(offset,0))
|
859
|
+
end
|
860
|
+
end
|
861
|
+
end
|
862
|
+
end
|
863
|
+
end
|
864
|
+
@structure_built = true
|
865
|
+
end
|
866
|
+
|
867
|
+
class << self
|
868
|
+
# Fields, singular and plural associations.
|
869
|
+
def properties
|
870
|
+
self.fields.merge(self.singular_associations.merge(self.plural_associations))
|
871
|
+
end
|
872
|
+
|
873
|
+
# Returns true if the +name+ (as symbol) is a name of a field.
|
874
|
+
def field?(name)
|
875
|
+
self.fields.keys.include?(name)
|
876
|
+
end
|
877
|
+
|
878
|
+
# Returns true if the +name+ (as symbol) is a name of a singular association.
|
879
|
+
def singular_association?(name)
|
880
|
+
self.singular_associations.keys.include?(name)
|
881
|
+
end
|
882
|
+
|
883
|
+
# Returns true if the +name+ (as symbol) is a name of a plural association.
|
884
|
+
def plural_association?(name)
|
885
|
+
self.plural_associations.keys.include?(name)
|
886
|
+
end
|
887
|
+
|
888
|
+
# Read index for the +property+ with +options+ from the database.
|
889
|
+
# If +key+ is given, the value for the key is returned.
|
890
|
+
# accessing the values for that key.
|
891
|
+
def index_for(property,options,key=nil)
|
892
|
+
index = instance_variable_get("@#{property}_index")
|
893
|
+
if index.nil?
|
894
|
+
index = database.read_index(self,property,options)
|
895
|
+
instance_variable_set("@#{property}_index",index)
|
896
|
+
end
|
897
|
+
if key
|
898
|
+
index[key]
|
899
|
+
else
|
900
|
+
index
|
901
|
+
end
|
902
|
+
end
|
903
|
+
|
904
|
+
# Sets the values in the index of the +property+ for
|
905
|
+
# the particular +key+. Method expects +fetch+ block and
|
906
|
+
# creates a CollectionProxy based on that block.
|
907
|
+
# The size of the collection is given as +count+.
|
908
|
+
def set_values_for(property,options,key,count,&fetch)
|
909
|
+
index_for(property,options)[key] = CollectionProxy.new(count,&fetch)
|
910
|
+
end
|
911
|
+
|
912
|
+
private
|
913
|
+
# Returns object of this class stored in the DB with given +rod_id+.
|
914
|
+
# Warning! If wrong rod_id is specified it might cause segmentation fault exception!
|
915
|
+
def get(rod_id)
|
916
|
+
object = cache[rod_id]
|
917
|
+
if object.nil?
|
918
|
+
object = self.new(rod_id)
|
919
|
+
cache[rod_id] = object
|
920
|
+
end
|
921
|
+
object
|
922
|
+
end
|
923
|
+
|
924
|
+
end
|
925
|
+
end
|
926
|
+
end
|