rod 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/Gemfile
ADDED
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
|
data/Rakefile
ADDED
@@ -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
|
+
|
data/changelog.txt
ADDED
@@ -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
|
data/lib/rod.rb
ADDED
@@ -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
|