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