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 ADDED
@@ -0,0 +1,2 @@
1
+ source "http://www.rubygems.org"
2
+ gemspec
data/README ADDED
@@ -0,0 +1,144 @@
1
+ = ROD -- Ruby Object Database
2
+
3
+ * http://github.com/apohllo/rod
4
+
5
+ == DESCRIPTION
6
+
7
+ ROD (Ruby Object Database) is library which aims at providing
8
+ fast access for data, which rarely changes.
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ * nice Ruby interface which mimicks Active Record
13
+ * Ruby-to-C on-the-fly translation based on mmap
14
+ * optimized for speed
15
+ * weak reference collections for easy memory reclaims
16
+ * segmented indices for short start-up time
17
+
18
+ * doesn't work on Windows
19
+
20
+ == SYNOPSIS:
21
+
22
+ ROD is designed for storing and accessing data which rarely changes.
23
+ It is an opposite of RDBMS as the data is not normalized.
24
+ It is an opposite of in-memory databases, since it is designed to cover
25
+ out of core data sets (10 GB and more).
26
+
27
+ The primary reason for designing it was to create storage facility for
28
+ natural language dictionaries and corpora. The data in a fully fledged dictionary
29
+ is interconnected in many ways, thus the relational model (joins) introduces
30
+ unacceptable performance hit. The size of corpora forces them to be kept
31
+ on disks. The in-memory data bases are unacceptable for larg corpora and
32
+ would require the data to be kept mostly in the operational memory,
33
+ which is not needed, while accessing dictionaries (in most cases only a friction
34
+ of the data is needed). That's why a storage facility which minimizes the
35
+ number of disk reads was designed. The Ruby interface facilitates it's
36
+ usage.
37
+
38
+ == REQUIREMENTS:
39
+
40
+ * RubyInline
41
+ * english
42
+ * ActiveModel
43
+ * weak_hash
44
+
45
+ == INSTALL
46
+
47
+ Grab from rubygems:
48
+
49
+ gem install rod
50
+
51
+ == BASIC USAGE:
52
+
53
+ class MyDatabase < Rod::Database
54
+ end
55
+
56
+ class Model < Rod::Model
57
+ database_class MyDatabase
58
+ end
59
+
60
+ class User < Model
61
+ field :name, :string
62
+ field :surname, :string, :index => true
63
+ field :age, :integer
64
+ has_one :account
65
+ has_many :files
66
+ end
67
+
68
+ class Account < Model
69
+ field :email, :string
70
+ field :login, :string, :index => true
71
+ field :password, :string
72
+ end
73
+
74
+ class File < Model
75
+ field :title, :string, :index => true
76
+ field :data, :string
77
+ end
78
+
79
+ MyDatabase.create_database("data")
80
+ user = User.new
81
+ user.name = 'Fred'
82
+ user.surname = 'Smith'
83
+ user.age = 22
84
+ account = Account.new
85
+ account.email = "fred@smith.org"
86
+ account.login = "fred"
87
+ account.password = "password"
88
+ file1 = File.new
89
+ file1.title = "Lady Gaga video"
90
+ file2.data = "0012220001..."
91
+ file2 = File.new
92
+ file2.title = "Pink Floyd video"
93
+ file2.data = "0012220001..."
94
+ user.account = account
95
+ user.files << file1
96
+ user.files << file2
97
+ user.store
98
+ account.store
99
+ file1.store
100
+ file2.store
101
+ MyDatabase.close_database
102
+
103
+ MyDatabase.open_database("data")
104
+ User.each do |user|
105
+ puts "Name: #{user.name} surname: #{user.surname}"
106
+ puts "login: #{user.account.login} e-mail: #{user.account.email}"
107
+ user.files.each do |file|
108
+ puts "File: #{file.title}
109
+ end
110
+ end
111
+
112
+ User[0] # gives first user
113
+ User.find_by_surname("Smith") # gives Fred
114
+ User.find_all_by_surname("Smith") # gives [Fred]
115
+ File[0].user # won't work - the data is not normalized
116
+
117
+ == LICENSE:
118
+
119
+ (The MIT License)
120
+
121
+ Copyright (c) 2008-2010 Aleksander Pohl
122
+
123
+ Permission is hereby granted, free of charge, to any person obtaining
124
+ a copy of this software and associated documentation files (the
125
+ 'Software'), to deal in the Software without restriction, including
126
+ without limitation the rights to use, copy, modify, merge, publish,
127
+ distribute, sublicense, and/or sell copies of the Software, and to
128
+ permit persons to whom the Software is furnished to do so, subject to
129
+ the following conditions:
130
+
131
+ The above copyright notice and this permission notice shall be
132
+ included in all copies or substantial portions of the Software.
133
+
134
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
135
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
136
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
137
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
138
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
139
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
140
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
141
+
142
+ == FEEDBACK
143
+
144
+ * mailto:apohllo@o2.pl
@@ -0,0 +1,58 @@
1
+ $:.unshift "lib"
2
+ require 'rod/constants'
3
+
4
+ task :default => [:install]
5
+
6
+ $gem_name = "rod"
7
+
8
+ desc "Build the gem"
9
+ task :build => :all_tests do
10
+ sh "gem build #$gem_name.gemspec"
11
+ FileUtils.mkdir("pkg") unless File.exist?("pkg")
12
+ sh "mv '#$gem_name-#{Rod::VERSION}.gem' pkg"
13
+ end
14
+
15
+ desc "Install the library at local machnie"
16
+ task :install => :build do
17
+ sh "sudo gem install pkg/#$gem_name-#{Rod::VERSION}.gem"
18
+ end
19
+
20
+ desc "Uninstall the library from local machnie"
21
+ task :uninstall do
22
+ sh "sudo gem uninstall #$gem_name"
23
+ end
24
+
25
+ task :all_tests => [:test,:spec,:regression_test] do
26
+ end
27
+
28
+ desc "Run performence tests"
29
+ task :perf do
30
+ sh "ruby tests/eff1_test.rb"
31
+ sh "ruby tests/eff2_test.rb"
32
+ sh "ruby tests/full_runs.rb"
33
+ end
34
+
35
+ desc "Run tests and specs"
36
+ task :test do
37
+ sh "ruby tests/save_struct.rb"
38
+ sh "ruby tests/load_struct.rb"
39
+ sh "ruby tests/unit/model.rb"
40
+ sh "ruby tests/unit/model_tests.rb"
41
+ sh "ruby tests/unit/abstract_database.rb"
42
+ end
43
+
44
+ # Should be removed some time -- specs should cover all these cases
45
+ task :regression_test do
46
+ sh "ruby tests/read_on_create.rb"
47
+ sh "ruby tests/check_strings.rb"
48
+ end
49
+
50
+ task :spec do
51
+ sh "bundle exec cucumber features/*"
52
+ end
53
+
54
+ desc "Clean"
55
+ task :clean do
56
+ sh "rm #$gem_name*.gem"
57
+ end
58
+
@@ -0,0 +1,99 @@
1
+ 0.6.0
2
+ - #64 index for associations
3
+ - Update legacy test to new API
4
+ - #26 check version of Rod library agains file version
5
+ - #1 initialization form hash (ActiveRecord style)
6
+ - #68 the indexed objects are stored in a collection proxy and are laizly fetched
7
+ - #9 has_many is lazy
8
+ no referenced element representation is created, utill the element is accessed
9
+ - #69 fix inheritence and polymorphic associations
10
+ - Refactoring (API change): change all internal calls to pass rod_id instead of position
11
+ - #46 Store only rod_id and class_id for referenced objects
12
+ - #66 after storing an object, clear it singular and plural associations to allow later garbage collection
13
+ - #57 better implementation of struct_name
14
+ - #61 excessive Symbol#to_s calls removal
15
+ - #61 excessive String#to_sym calls removal
16
+ - #58 compact implementation of update_plural
17
+ - #41 structs associated with the object is not wrapped into another object
18
+ - Tests for #56
19
+ - #55 associated object is fetched from DB, even though it is present
20
+ - Change Willma to Wilma in tests
21
+ - #52 load bucket for segmented index during DB creation
22
+ - Don't delete old buckets when the DB is closed
23
+ - Fix: load bucket when the segmented index is created
24
+ - #59 Change WeakHash to SimpleWeakHash
25
+ - #54 don't relay on Ruby#hash in implementation of segmented index
26
+ - #53 Remove segmented index files when the DB is created
27
+ 0.5.5
28
+ - segmented index #31
29
+ - don't cache objects when they are stored in the DB #19
30
+ - #33 pre-allocate larger data segments
31
+ - #42 storage of index in regular file
32
+ - #23 change all runtime exceptions to RodExceptions
33
+ - Chagne page_size variable to page_size call
34
+ - #4 append of database (experimental)
35
+ - Refactor index read and write #42
36
+ - Add guards for read-only data
37
+ - #39 remove page count from metadata
38
+ - #38 invalid size for munmap when closing DB
39
+ - #35 - meta-data is stored in yaml
40
+ 0.5.4
41
+ - default implementation of to_s
42
+ - DB data is stored in a DB, not a single file #36 #37
43
+ - removal of legacy tests
44
+ - index flushing when the DB is created
45
+ 0.5.3
46
+ - implementation of field serialization
47
+ - refactoring of field storage
48
+ - implementation of enumerator and enumerable
49
+ 0.5.2
50
+ - polymorphic associations
51
+ - fix C code: change INT2NUM to UINT2NUM for unsigned values
52
+ - make index scope error for model more descriptive
53
+ - features for nil associations
54
+ - store version of the library in Ruby code
55
+ - make Rakefile more useful
56
+ - rod.rb explicitly enumerates the required files
57
+ 0.5.1
58
+ - force ActiveSupport::Dependencis to use regular require
59
+ - fix initialization of models with indices
60
+ - add more info for basic feature
61
+ 0.5.0
62
+ - simultaneous usage of many databases
63
+ - refactoring of service and model
64
+ - remove page from string C definition
65
+ - remove index packign/unpacking
66
+ - inheritence of attributes and associations
67
+ - features for all basic functions
68
+ 0.4.4
69
+ - minor fix: count objects while storing
70
+ - remove separate zero-string tests
71
+ - Fred feature uses model step definitions
72
+ - model step definitions refactoring
73
+ - remove Ruby inline generated files during tests
74
+ 0.4.3
75
+ - some test changed into features specification
76
+ - default implementation of == for Model
77
+ - scope check for []
78
+ - allow for purging subclass information when closing database
79
+ - cache clearing
80
+ - unsued C methods are generated only in development mode
81
+ - updated dependencies
82
+ 0.4.2
83
+ - clear Gemfile
84
+ - make clear statements about dev. dependencies
85
+ 0.4.1
86
+ - allow to skip validation
87
+ - tests updated for Ruby 1.9.2
88
+ - Gemfile added
89
+ 0.4.0
90
+ - page offset removed from string information (merged with string)
91
+ - cache clearing turned off
92
+ 0.3.1
93
+ - build_structure is called when the DB is created/opened
94
+ - change message when not all objects are stored: only the number of objects
95
+ - when the DB is closed only the number of not stored objects is reported
96
+ 0.3.0
97
+ - data is stored in separated files during creation
98
+ 0.2.0
99
+ - uses ActiveModel for validation
@@ -0,0 +1,21 @@
1
+ require 'inline'
2
+ require 'english/inflect'
3
+ require 'simple_weak_hash'
4
+ require 'active_model'
5
+ require 'active_support/dependencies'
6
+
7
+ # XXX This should be done in a different way, since a library should not
8
+ # impose on a user of another library specific way of using it.
9
+ # See #21
10
+ ActiveSupport::Dependencies.mechanism = :require
11
+
12
+ require 'rod/abstract_database'
13
+ require 'rod/constants'
14
+ require 'rod/database'
15
+ require 'rod/exception'
16
+ require 'rod/join_element'
17
+ require 'rod/collection_proxy'
18
+ require 'rod/model'
19
+ require 'rod/string_element'
20
+ require 'rod/string_ex'
21
+ require 'rod/segmented_index'
@@ -0,0 +1,369 @@
1
+ require 'singleton'
2
+ require 'yaml'
3
+ require 'rod/segmented_index'
4
+
5
+ module Rod
6
+ # This class implements the database abstraction, i.e. it
7
+ # is a mediator between some model (a set of classes) and
8
+ # the generated C code, implementing the data storage functionality.
9
+ class AbstractDatabase
10
+ # This class is a singleton, since in a given time instant there
11
+ # is only one database (one file/set of files) storing data of
12
+ # a given model (set of classes).
13
+ include Singleton
14
+
15
+ # Initializes the classes linked with this database and the handler.
16
+ def initialize
17
+ @classes ||= self.class.special_classes
18
+ @handler = nil
19
+ end
20
+
21
+ #########################################################################
22
+ # Public API
23
+ #########################################################################
24
+
25
+ # Returns whether the database is opened.
26
+ def opened?
27
+ not @handler.nil?
28
+ end
29
+
30
+ # The DB open mode.
31
+ def readonly_data?
32
+ @readonly
33
+ end
34
+
35
+ # Creates the database at specified +path+, which allows
36
+ # for Rod::Model#store calls to be performed.
37
+ #
38
+ # The database is created for all classes, which have this
39
+ # database configured via Rod::Model#database_class call
40
+ # (this configuration is by default inherited in subclasses,
41
+ # so it have to be called only in the root class of given model).
42
+ #
43
+ # WARNING: all files in the DB directory are removed during DB creation!
44
+ def create_database(path)
45
+ raise DatabaseError.new("Database already opened.") unless @handler.nil?
46
+ @readonly = false
47
+ self.classes.each{|s| s.send(:build_structure)}
48
+ @path = canonicalize_path(path)
49
+ # XXX maybe should be more careful?
50
+ if File.exist?(@path)
51
+ Dir.glob("#{@path}**/*").each do |file_name|
52
+ File.delete(file_name) unless File.directory?(file_name)
53
+ end
54
+ else
55
+ Dir.mkdir(@path)
56
+ end
57
+ generate_c_code(@path, classes)
58
+ @handler = _init_handler(@path)
59
+ _create(@handler)
60
+ end
61
+
62
+ # Opens the database at +path+ for readonly mode. This allows
63
+ # for Rod::Model.count, Rod::Model.each, and similar calls.
64
+ #
65
+ # By default the database is opened in +readonly+ mode. You
66
+ # can change it by passing +false+ as the second argument.
67
+ def open_database(path,readonly=true)
68
+ raise DatabaseError.new("Database already opened.") unless @handler.nil?
69
+ @readonly = readonly
70
+ self.classes.each{|s| s.send(:build_structure)}
71
+ @path = canonicalize_path(path)
72
+ generate_c_code(@path, classes)
73
+ metadata = {}
74
+ File.open(@path + DATABASE_FILE) do |input|
75
+ metadata = YAML::load(input)
76
+ end
77
+ unless valid_version?(metadata["Rod"][:version])
78
+ raise RodException.new("Incompatible versions - library #{VERSION} vs. file #{metatdata["Rod"][:version]}")
79
+ end
80
+ @handler = _init_handler(@path)
81
+ self.classes.each do |klass|
82
+ meta = metadata[klass.name]
83
+ if meta.nil?
84
+ # new class
85
+ next
86
+ end
87
+ set_count(klass,meta[:count])
88
+ file_size = File.new(klass.path_for_data(@path)).size
89
+ unless file_size % _page_size == 0
90
+ raise DatabaseError.new("Size of data file of #{klass} is invalid: #{file_size}")
91
+ end
92
+ set_page_count(klass,file_size / _page_size)
93
+ end
94
+ _open(@handler)
95
+ end
96
+
97
+ # Closes the database.
98
+ #
99
+ # If the +purge_classes+ flag is set to true, the information about the classes
100
+ # linked with this database is removed. This is important for testing, when
101
+ # classes with same names have different definitions.
102
+ def close_database(purge_classes=false)
103
+ raise DatabaseError.new("Database not opened.") if @handler.nil?
104
+
105
+ unless readonly_data?
106
+ unless referenced_objects.select{|k, v| not v.empty?}.size == 0
107
+ raise DatabaseError.new("Not all associations have been stored: #{referenced_objects.size} objects")
108
+ end
109
+ metadata = {}
110
+ rod_data = metadata["Rod"] = {}
111
+ rod_data[:version] = VERSION
112
+ self.classes.each do |klass|
113
+ meta = metadata[klass.name] = {}
114
+ meta[:count] = count(klass)
115
+ next if special_class?(klass)
116
+ # fields
117
+ fields = meta[:fields] = {} unless klass.fields.empty?
118
+ klass.fields.each do |field,options|
119
+ fields[field] = {}
120
+ fields[field][:options] = options
121
+ write_index(klass,field,options) if options[:index]
122
+ end
123
+ # singular_associations
124
+ has_one = meta[:has_one] = {} unless klass.singular_associations.empty?
125
+ klass.singular_associations.each do |name,options|
126
+ has_one[name] = {}
127
+ has_one[name][:options] = options
128
+ write_index(klass,name,options) if options[:index]
129
+ end
130
+ # plural_associations
131
+ has_many = meta[:has_many] = {} unless klass.plural_associations.empty?
132
+ klass.plural_associations.each do |name,options|
133
+ has_many[name] = {}
134
+ has_many[name][:options] = options
135
+ write_index(klass,name,options) if options[:index]
136
+ end
137
+ end
138
+ File.open(@path + DATABASE_FILE,"w") do |out|
139
+ out.puts(YAML::dump(metadata))
140
+ end
141
+ end
142
+ _close(@handler)
143
+ @handler = nil
144
+ # clear cached data
145
+ self.clear_cache
146
+ if purge_classes
147
+ @classes = self.class.special_classes
148
+ end
149
+ end
150
+
151
+ # Clears the cache of the database.
152
+ def clear_cache
153
+ classes.each{|c| c.cache.send(:__get_hash__).clear}
154
+ end
155
+
156
+ #########################################################################
157
+ # 'Private' API
158
+ #########################################################################
159
+
160
+ # "Stack" of objects which are referenced by other objects during store,
161
+ # but are not yet stored.
162
+ def referenced_objects
163
+ @referenced_objects ||= {}
164
+ end
165
+
166
+
167
+ # Adds the +klass+ to the set of classes linked with this database.
168
+ def add_class(klass)
169
+ @classes << klass unless @classes.include?(klass)
170
+ end
171
+
172
+ # Remove the +klass+ from the set of classes linked with this database.
173
+ def remove_class(klass)
174
+ unless @classes.include?(klass)
175
+ raise DatabaseError.new("Class #{klass} is not linked with #{self}!")
176
+ end
177
+ @classes.delete(klass)
178
+ end
179
+
180
+ # Returns join index with +index+ and +offset+.
181
+ def join_index(offset, index)
182
+ _join_element_index(offset, index, @handler)
183
+ end
184
+
185
+ # Returns polymorphic join index with +index+ and +offset+.
186
+ # This is the rod_id of the object referenced via
187
+ # a polymorphic has many association for one instance.
188
+ def polymorphic_join_index(offset, index)
189
+ _polymorphic_join_element_index(offset, index, @handler)
190
+ end
191
+
192
+ # Returns polymorphic join class id with +index+ and +offset+.
193
+ # This is the class_id (name_hash) of the object referenced via
194
+ # a polymorphic has many association for one instance.
195
+ def polymorphic_join_class(offset, index)
196
+ _polymorphic_join_element_class(offset, index, @handler)
197
+ end
198
+
199
+ # Sets the +object_id+ of the join element with +offset+ and +index+.
200
+ def set_join_element_id(offset,index,object_id)
201
+ raise DatabaseError.new("Readonly database.") if readonly_data?
202
+ _set_join_element_offset(offset, index, object_id, @handler)
203
+ end
204
+
205
+ # Sets the +object_id+ and +class_id+ of the
206
+ # polymorphic join element with +offset+ and +index+.
207
+ def set_polymorphic_join_element_id(offset,index,object_id,class_id)
208
+ raise DatabaseError.new("Readonly database.") if readonly_data?
209
+ _set_polymorphic_join_element_offset(offset, index, object_id,
210
+ class_id, @handler)
211
+ end
212
+
213
+ # Returns the string of given +length+ starting at given +offset+.
214
+ def read_string(length, offset)
215
+ # TODO the encoding should be stored in the DB
216
+ # or configured globally
217
+ _read_string(length, offset, @handler).force_encoding("utf-8")
218
+ end
219
+
220
+ # Stores the string in the DB encoding it to utf-8.
221
+ def set_string(value)
222
+ raise DatabaseError.new("Readonly database.") if readonly_data?
223
+ _set_string(value.encode("utf-8"),@handler)
224
+ end
225
+
226
+ # Returns the number of objects for given +klass+.
227
+ def count(klass)
228
+ send("_#{klass.struct_name}_count",@handler)
229
+ end
230
+
231
+ # Sets the number of objects for given +klass+.
232
+ def set_count(klass,value)
233
+ send("_#{klass.struct_name}_count=",@handler,value)
234
+ end
235
+
236
+ # Sets the number of pages allocated for given +klass+.
237
+ def set_page_count(klass,value)
238
+ send("_#{klass.struct_name}_page_count=",@handler,value)
239
+ end
240
+
241
+ # Reads index of +field+ (with +options+) for +klass+.
242
+ def read_index(klass,field,options)
243
+ case options[:index]
244
+ when :flat,true
245
+ begin
246
+ File.open(klass.path_for_index(@path,field,options)) do |input|
247
+ return {} if input.size == 0
248
+ return Marshal.load(input)
249
+ end
250
+ rescue Errno::ENOENT
251
+ return {}
252
+ end
253
+ when :segmented
254
+ return SegmentedIndex.new(klass.path_for_index(@path,field,options))
255
+ else
256
+ raise RodException.new("Invalid index type '#{options[:index]}'.")
257
+ end
258
+ end
259
+
260
+ # Store index of +field+ (with +options+) of +klass+ in the database.
261
+ # There are two types of indices:
262
+ # * +:flat+ - marshalled index is stored in one file
263
+ # * +:segmented+ - marshalled index is stored in many files
264
+ def write_index(klass,property,options)
265
+ raise DatabaseError.new("Readonly database.") if readonly_data?
266
+ class_index = klass.index_for(property,options)
267
+ class_index.each do |key,ids|
268
+ unless ids.is_a?(CollectionProxy)
269
+ proxy = CollectionProxy.new(ids[1]) do |index|
270
+ [join_index(ids[0],index), klass]
271
+ end
272
+ else
273
+ proxy = ids
274
+ end
275
+ offset = _allocate_join_elements(proxy.size,@handler)
276
+ proxy.each_id.with_index do |rod_id,index|
277
+ set_join_element_id(offset, index, rod_id)
278
+ end
279
+ class_index[key] = [offset,proxy.size]
280
+ end
281
+ case options[:index]
282
+ when :flat,true
283
+ File.open(klass.path_for_index(@path,property,options),"w") do |out|
284
+ out.puts(Marshal.dump(class_index))
285
+ end
286
+ when :segmented
287
+ path = klass.path_for_index(@path,property,options)
288
+ if class_index.is_a?(Hash)
289
+ index = SegmentedIndex.new(path)
290
+ class_index.each{|k,v| index[k] = v}
291
+ else
292
+ index = class_index
293
+ end
294
+ index.save
295
+ index = nil
296
+ else
297
+ raise RodException.new("Invalid index type '#{options[:index]}'.")
298
+ end
299
+ end
300
+
301
+ # Store the object in the database.
302
+ def store(klass,object)
303
+ raise DatabaseError.new("Readonly database.") if readonly_data?
304
+ send("_store_" + klass.struct_name,object,@handler)
305
+ # set fields' values
306
+ object.class.fields.each do |name,options|
307
+ # rod_id is set during _store
308
+ object.update_field(name) unless name == "rod_id"
309
+ end
310
+ # set ids of objects referenced via singular associations
311
+ object.class.singular_associations.each do |name,options|
312
+ object.update_singular_association(name,object.send(name))
313
+ end
314
+ # set ids of objects referenced via plural associations
315
+ object.class.plural_associations.each do |name,options|
316
+ elements = object.send(name) || []
317
+ if options[:polymorphic]
318
+ offset = _allocate_polymorphic_join_elements(elements.size,@handler)
319
+ else
320
+ offset = _allocate_join_elements(elements.size,@handler)
321
+ end
322
+ object.update_count_and_offset(name,elements.size,offset)
323
+ object.update_plural_association(name,elements)
324
+ end
325
+ end
326
+
327
+ # Prints the layout of the pages in memory and other
328
+ # internal data of the model.
329
+ def print_layout
330
+ raise DatabaseError.new("Database not opened.") if @handler.nil?
331
+ _print_layout(@handler)
332
+ end
333
+
334
+ # Prints the last error of system call.
335
+ def print_system_error
336
+ _print_system_error
337
+ end
338
+
339
+ protected
340
+
341
+ # Checks if the version of the library is valid.
342
+ # Consult https://github.com/apohllo/rod/wiki for versioning scheme.
343
+ def valid_version?(version)
344
+ file = version.split(".")
345
+ library = VERSION.split(".")
346
+ return false if file[0] != library[0] || file[1] != library[1]
347
+ if library[1].to_i.even?
348
+ return true
349
+ else
350
+ return file[2] == library[2]
351
+ end
352
+ end
353
+
354
+ # Returns collected subclasses.
355
+ def classes
356
+ @classes.sort{|c1,c2| c1.to_s <=> c2.to_s}
357
+ end
358
+
359
+ # Retruns the path to the DB as a name of a directory.
360
+ def canonicalize_path(path)
361
+ path + "/" unless path[-1] == "/"
362
+ end
363
+
364
+ # Special classes used by the database.
365
+ def self.special_classes
366
+ [JoinElement, PolymorphicJoinElement, StringElement]
367
+ end
368
+ end
369
+ end