rod 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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