rod 0.6.3 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +2 -0
- data/README.rdoc +27 -7
- data/changelog.txt +6 -0
- data/contributors.txt +2 -1
- data/features/hash_indexing.feature +172 -0
- data/features/steps/model.rb +3 -3
- data/lib/rod/abstract_database.rb +101 -58
- data/lib/rod/constants.rb +2 -1
- data/lib/rod/database.rb +9 -22
- data/lib/rod/exception.rb +4 -0
- data/lib/rod/index/base.rb +2 -0
- data/lib/rod/index/flat_index.rb +1 -0
- data/lib/rod/index/hash_index.rb +266 -0
- data/lib/rod/index/segmented_index.rb +1 -0
- data/lib/rod/model.rb +75 -10
- data/lib/rod.rb +1 -0
- data/tests/migration_create.rb +13 -7
- data/tests/migration_migrate.rb +4 -4
- data/tests/migration_model1.rb +3 -0
- data/tests/migration_model2.rb +11 -2
- data/tests/migration_verify.rb +14 -2
- metadata +5 -2
data/.travis.yml
CHANGED
data/README.rdoc
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
* http://github.com/apohllo/rod
|
4
4
|
|
5
|
+
== WARNING
|
6
|
+
|
7
|
+
The 0.7.x branch is a development branch -- incompatibilities between library
|
8
|
+
releases might be introduced (both in API and data schema).
|
9
|
+
You are advised to use the latest release of 0.6.x branch.
|
10
|
+
|
5
11
|
== DESCRIPTION
|
6
12
|
|
7
13
|
ROD (Ruby Object Database) is library which aims at providing
|
@@ -13,11 +19,11 @@ fast access for data, which rarely changes.
|
|
13
19
|
* Ruby-to-C on-the-fly translation based on mmap and RubyInline
|
14
20
|
* optimized for (reading) speed
|
15
21
|
* weak reference collections for easy memory reclaims
|
16
|
-
*
|
22
|
+
* Berkeley DB hash index for the best index performance
|
17
23
|
* compatibility check of library version
|
18
24
|
* compatibility check of data model
|
19
25
|
* autogeneration of model (based on the database metadata)
|
20
|
-
* automatic model migrations (addition/removal of properties
|
26
|
+
* automatic model migrations (limitied to addition/removal of properties and indexes)
|
21
27
|
* full update of the database (removal of objects not available yet)
|
22
28
|
* databases interlinking (via direct links or inverted indices)
|
23
29
|
|
@@ -25,7 +31,6 @@ fast access for data, which rarely changes.
|
|
25
31
|
|
26
32
|
* tested mostly on 64-bit systems
|
27
33
|
* doesn't work on Windows
|
28
|
-
* some space is wasted when database is re-opended in read/write mode
|
29
34
|
|
30
35
|
== SYNOPSIS:
|
31
36
|
|
@@ -52,13 +57,28 @@ number of disk reads was designed. The Ruby interface facilitates it's usage.
|
|
52
57
|
* english
|
53
58
|
* ActiveModel
|
54
59
|
* bsearch
|
60
|
+
* Berkeley DB
|
55
61
|
|
56
62
|
== INSTALL
|
57
63
|
|
58
|
-
|
64
|
+
1. Install Berkeley DB
|
65
|
+
|
66
|
+
http://www.oracle.com/technetwork/database/berkeleydb/downloads/index.html
|
67
|
+
|
68
|
+
2. Install rod gem from rubygems:
|
59
69
|
|
60
70
|
gem install rod
|
61
71
|
|
72
|
+
== TROUBLESHOOTING
|
73
|
+
|
74
|
+
If you get the following error during library usage:
|
75
|
+
|
76
|
+
error: db.h: No such file or directory
|
77
|
+
|
78
|
+
then you don't have Berkeley DB installed or its header fiels are not available
|
79
|
+
on the default path. Make sure that the library is installed and the headers
|
80
|
+
are available.
|
81
|
+
|
62
82
|
== BASIC USAGE:
|
63
83
|
|
64
84
|
class MyDatabase < Rod::Database
|
@@ -70,7 +90,7 @@ Grab from rubygems:
|
|
70
90
|
|
71
91
|
class User < Model
|
72
92
|
field :name, :string
|
73
|
-
field :surname, :string, :index => :
|
93
|
+
field :surname, :string, :index => :hash
|
74
94
|
field :age, :integer
|
75
95
|
has_one :account
|
76
96
|
has_many :files
|
@@ -78,12 +98,12 @@ Grab from rubygems:
|
|
78
98
|
|
79
99
|
class Account < Model
|
80
100
|
field :email, :string
|
81
|
-
field :login, :string, :index => :
|
101
|
+
field :login, :string, :index => :hash
|
82
102
|
field :password, :string
|
83
103
|
end
|
84
104
|
|
85
105
|
class File < Model
|
86
|
-
field :title, :string, :index => :
|
106
|
+
field :title, :string, :index => :hash
|
87
107
|
field :data, :string
|
88
108
|
end
|
89
109
|
|
data/changelog.txt
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
0.7.0
|
2
|
+
- #142 migrate indices during model migration
|
3
|
+
- #134 Berkeley DB Hash for indexing properties
|
4
|
+
- #137 move #special_class? to AbstractDatabase
|
5
|
+
- #138 move development_mode accessor to AbstractDatabase
|
6
|
+
- #136 use (DB) #opened? instead of @handler.nil?
|
1
7
|
0.6.3
|
2
8
|
- #94 indexing of unstored object in plural assoc
|
3
9
|
- #128 incompatible schema message is more specific
|
data/contributors.txt
CHANGED
@@ -0,0 +1,172 @@
|
|
1
|
+
Feature: Access to objects with hash indices.
|
2
|
+
ROD allows for accessing objects via fields with hash indices,
|
3
|
+
which are useful for indices with millions of keys.
|
4
|
+
These are split accross multiple files for faster load-time.
|
5
|
+
Background:
|
6
|
+
Given the library works in development mode
|
7
|
+
|
8
|
+
Scenario: indexing with hash index
|
9
|
+
Rod should allow to access objects via values of their fields,
|
10
|
+
for which indices were built.
|
11
|
+
Given the class space is cleared
|
12
|
+
And the model is connected with the default database
|
13
|
+
And a class Caveman has a name field of type string with hash index
|
14
|
+
And a class Caveman has an age field of type integer with hash index
|
15
|
+
And a class Caveman has an identifier field of type ulong with hash index
|
16
|
+
And a class Caveman has a height field of type float with hash index
|
17
|
+
When database is created
|
18
|
+
And I create a Caveman
|
19
|
+
And his name is 'Fred'
|
20
|
+
And his age is '25'
|
21
|
+
And his identifier is '111122223333'
|
22
|
+
And his height is '1.86'
|
23
|
+
And I store him in the database
|
24
|
+
And I create another Caveman
|
25
|
+
And his name is 'Barney'
|
26
|
+
And his age is '26'
|
27
|
+
And his identifier is '111122224444'
|
28
|
+
And his height is '1.67'
|
29
|
+
And I store him in the database
|
30
|
+
And I create another Caveman
|
31
|
+
And his name is 'Wilma'
|
32
|
+
And his age is '25'
|
33
|
+
And his identifier is '111122225555'
|
34
|
+
And his height is '1.67'
|
35
|
+
And I store her in the database
|
36
|
+
And I reopen database for reading
|
37
|
+
Then there should be 3 Caveman(s)
|
38
|
+
And there should be 1 Caveman with 'Fred' name
|
39
|
+
And there should be 1 Caveman with 'Wilma' name
|
40
|
+
And there should be 1 Caveman with 'Barney' name
|
41
|
+
And there should be 2 Caveman(s) with '25' age
|
42
|
+
And there should be 1 Caveman with '26' age
|
43
|
+
And there should be 1 Caveman with '111122223333' identifier
|
44
|
+
And there should be 1 Caveman with '111122224444' identifier
|
45
|
+
And there should be 1 Caveman with '111122225555' identifier
|
46
|
+
And there should be 2 Caveman(s) with '1.67' height
|
47
|
+
And there should be 1 Caveman with '1.86' height
|
48
|
+
|
49
|
+
# Test re-creation of the database
|
50
|
+
When database is created
|
51
|
+
And I create a Caveman
|
52
|
+
And his name is 'Fred'
|
53
|
+
And I store him in the database
|
54
|
+
And I create another Caveman
|
55
|
+
And his name is 'Barney'
|
56
|
+
And I store him in the database
|
57
|
+
And I create another Caveman
|
58
|
+
And her name is 'Wilma'
|
59
|
+
And I store her in the database
|
60
|
+
And I reopen database for reading
|
61
|
+
Then there should be 3 Caveman(s)
|
62
|
+
And there should be 1 Caveman with 'Fred' name
|
63
|
+
And there should be 1 Caveman with 'Wilma' name
|
64
|
+
And there should be 1 Caveman with 'Barney' name
|
65
|
+
|
66
|
+
Scenario: extending the DB when hash index is used
|
67
|
+
Rod should allow to extend the DB when the hash index is used.
|
68
|
+
The index should be properly updated.
|
69
|
+
Given the class space is cleared
|
70
|
+
And the model is connected with the default database
|
71
|
+
And a class Caveman has a name field of type string with hash index
|
72
|
+
When database is created
|
73
|
+
And I create a Caveman
|
74
|
+
And his name is 'Fred'
|
75
|
+
And I store him in the database
|
76
|
+
And I create another Caveman
|
77
|
+
And his name is 'Barney'
|
78
|
+
And I store him in the database
|
79
|
+
And I reopen database
|
80
|
+
And I create another Caveman
|
81
|
+
And her name is 'Wilma'
|
82
|
+
And I store her in the database
|
83
|
+
And I create another Caveman
|
84
|
+
And his name is 'Fred'
|
85
|
+
And I store him in the database
|
86
|
+
And I reopen database for reading
|
87
|
+
Then there should be 4 Caveman(s)
|
88
|
+
And there should be 1 Caveman with 'Wilma' name
|
89
|
+
And there should be 2 Caveman(s) with 'Fred' name
|
90
|
+
And there should be 1 Caveman with 'Barney' name
|
91
|
+
|
92
|
+
Scenario: indexing of fields with different DBs for the same model with hash index
|
93
|
+
The contents of indices should be fulshed when the database is reopened.
|
94
|
+
Given the class space is cleared
|
95
|
+
And the model is connected with the default database
|
96
|
+
And a class Caveman has a name field of type string with hash index
|
97
|
+
When database is created
|
98
|
+
And I create a Caveman
|
99
|
+
And his name is 'Fred'
|
100
|
+
And I store him in the database
|
101
|
+
And I create another Caveman
|
102
|
+
And his name is 'Fred'
|
103
|
+
And I store him in the database
|
104
|
+
And I create another Caveman
|
105
|
+
And his name is 'Fred'
|
106
|
+
And I store him in the database
|
107
|
+
And I reopen database for reading
|
108
|
+
And I access the Caveman name index
|
109
|
+
And database is created in location2
|
110
|
+
And I create a Caveman
|
111
|
+
And his name is 'Wilma'
|
112
|
+
And I store him in the database
|
113
|
+
And I create another Caveman
|
114
|
+
And his name is 'Wilma'
|
115
|
+
And I store him in the database
|
116
|
+
And I create another Caveman
|
117
|
+
And his name is 'Wilma'
|
118
|
+
And I store him in the database
|
119
|
+
And I reopen database for reading in location2
|
120
|
+
Then there should be 3 Caveman(s)
|
121
|
+
And there should be 3 Caveman(s) with 'Wilma' name
|
122
|
+
And there should be 0 Caveman(s) with 'Fred' name
|
123
|
+
|
124
|
+
Scenario: indexing of particular values with hash index
|
125
|
+
Given the class space is cleared
|
126
|
+
And the model is connected with the default database
|
127
|
+
And a class Caveman has a name field of type string with hash index
|
128
|
+
And a class Caveman has a surname field of type string with hash index
|
129
|
+
And a class Caveman has a login field of type string with hash index
|
130
|
+
And a class Caveman has an age field of type integer with hash index
|
131
|
+
When database is created
|
132
|
+
And I create and store the following Caveman(s):
|
133
|
+
| name | surname | login | age |
|
134
|
+
| John | Smith | john | 12 |
|
135
|
+
| Lara | Croft | lara | 23 |
|
136
|
+
| Adam | Parker | adam | 12 |
|
137
|
+
| Adam | | noob1 | 33 |
|
138
|
+
| | | noob2 | -1 |
|
139
|
+
| | Adam | noob1 | 33 |
|
140
|
+
And I reopen database for reading
|
141
|
+
Then there should be 6 Caveman(s)
|
142
|
+
And there should be 1 Caveman with 'John' name
|
143
|
+
And there should be 2 Caveman(s) with 'Adam' name
|
144
|
+
And there should be 2 Caveman(s) with '12' age
|
145
|
+
And there should be 1 Caveman with '-1' age
|
146
|
+
And there should be 2 Caveman(s) with '' name
|
147
|
+
And there should be 2 Caveman(s) with '' surname
|
148
|
+
|
149
|
+
Scenario: multiple object with indexed fields with hash index
|
150
|
+
The database should properly store thausands of objects with some indexed fields.
|
151
|
+
Given the class space is cleared
|
152
|
+
And the model is connected with the default database
|
153
|
+
And a class User has a name field of type string with hash index
|
154
|
+
And a class User has a surname field of type string with hash index
|
155
|
+
And a class User has an age field of type integer
|
156
|
+
When database is created
|
157
|
+
And I create a User
|
158
|
+
And his name is 'John'
|
159
|
+
And his surname is 'Smith'
|
160
|
+
And his age is '21'
|
161
|
+
And I store him in the database 1000 times
|
162
|
+
And I create a User
|
163
|
+
And her name is 'Lara'
|
164
|
+
And her surname is 'Croft'
|
165
|
+
And her age is '23'
|
166
|
+
And I store her in the database 1000 times
|
167
|
+
And I reopen database for reading
|
168
|
+
Then there should be 2000 User(s)
|
169
|
+
Then there should be 1000 User(s) with 'John' name
|
170
|
+
Then there should be 1000 User(s) with 'Smith' surname
|
171
|
+
Then there should be 1000 User(s) with 'Lara' name
|
172
|
+
Then there should be 1000 User(s) with 'Croft' surname
|
data/features/steps/model.rb
CHANGED
@@ -93,8 +93,8 @@ Given /^a class (\w+) inherits from ([\w:]+)$/ do |name1,name2|
|
|
93
93
|
end
|
94
94
|
|
95
95
|
Given /^a class (\w+) has an? (\w+) field of type (\w+)( with (\w+) index)?$/ do |class_name,field,type,index,index_type|
|
96
|
-
index_type = index_type == "flat" ? :flat : :segmented
|
97
96
|
if index
|
97
|
+
index_type = index_type.to_sym
|
98
98
|
get_class(class_name).send(:field,field.to_sym,type.to_sym,:index => index_type)
|
99
99
|
else
|
100
100
|
get_class(class_name).send(:field,field.to_sym,type.to_sym)
|
@@ -110,7 +110,7 @@ Given /^a class (\w+) has one (\w+ )?(\w+)( with (\w+) index)?$/ do |class_name,
|
|
110
110
|
end
|
111
111
|
end
|
112
112
|
unless index.nil?
|
113
|
-
index_type =
|
113
|
+
index_type = index_type.to_sym
|
114
114
|
options[:index] = index_type
|
115
115
|
end
|
116
116
|
get_class(class_name).send(:has_one,assoc.to_sym,options)
|
@@ -125,7 +125,7 @@ Given /^a class (\w+) has many (\w+ )?(\w+)( with (\w+) index)?$/ do |class_name
|
|
125
125
|
end
|
126
126
|
end
|
127
127
|
unless index.nil?
|
128
|
-
index_type =
|
128
|
+
index_type = index_type.to_sym
|
129
129
|
options[:index] = index_type
|
130
130
|
end
|
131
131
|
get_class(class_name).send(:has_many,assoc.to_sym,options)
|
@@ -21,12 +21,27 @@ module Rod
|
|
21
21
|
# The path which the database instance is located on.
|
22
22
|
attr_reader :path
|
23
23
|
|
24
|
+
# This flag indicates, if Database and Model works in development
|
25
|
+
# mode, i.e. the dynamically loaded library has a unique, different id each time
|
26
|
+
# the rod library is used.
|
27
|
+
@@rod_development_mode = false
|
28
|
+
|
24
29
|
# Initializes the classes linked with this database and the handler.
|
25
30
|
def initialize
|
26
31
|
@classes ||= self.special_classes
|
27
32
|
@handler = nil
|
28
33
|
end
|
29
34
|
|
35
|
+
# Writer of the +rod_development_mode+ flag.
|
36
|
+
def self.development_mode=(value)
|
37
|
+
@@rod_development_mode = value
|
38
|
+
end
|
39
|
+
|
40
|
+
# Reader of the +rod_development_mode+ flag.
|
41
|
+
def self.development_mode
|
42
|
+
@@rod_development_mode
|
43
|
+
end
|
44
|
+
|
30
45
|
#########################################################################
|
31
46
|
# Public API
|
32
47
|
#########################################################################
|
@@ -51,7 +66,7 @@ module Rod
|
|
51
66
|
#
|
52
67
|
# WARNING: all files in the DB directory are removed during DB creation!
|
53
68
|
def create_database(path)
|
54
|
-
raise DatabaseError.new("Database already opened.")
|
69
|
+
raise DatabaseError.new("Database already opened.") if opened?
|
55
70
|
@readonly = false
|
56
71
|
@path = canonicalize_path(path)
|
57
72
|
if File.exist?(@path)
|
@@ -87,25 +102,14 @@ module Rod
|
|
87
102
|
# the classes from the database metadata. If module given, the classes
|
88
103
|
# are generated withing the module.
|
89
104
|
def open_database(path,options={:readonly => true})
|
90
|
-
raise DatabaseError.new("Database already opened.")
|
105
|
+
raise DatabaseError.new("Database already opened.") if opened?
|
91
106
|
options = convert_options(options)
|
92
107
|
@readonly = options[:readonly]
|
93
108
|
@path = canonicalize_path(path)
|
94
|
-
@metadata =
|
95
|
-
File.open(@path + DATABASE_FILE) do |input|
|
96
|
-
@metadata = YAML::load(input)
|
97
|
-
end
|
98
|
-
unless valid_version?(@metadata["Rod"][:version])
|
99
|
-
raise IncompatibleVersion.new("Incompatible versions - library #{VERSION} vs. " +
|
100
|
-
"file #{metatdata["Rod"][:version]}")
|
101
|
-
end
|
109
|
+
@metadata = load_metadata
|
102
110
|
if options[:generate]
|
103
111
|
module_instance = (options[:generate] == true ? Object : options[:generate])
|
104
112
|
generate_classes(module_instance)
|
105
|
-
elsif options[:migrate]
|
106
|
-
create_legacy_classes
|
107
|
-
FileUtils.cp(@path + DATABASE_FILE, @path + DATABASE_FILE + LEGACY_DATA_SUFFIX)
|
108
|
-
remove_files(self.inline_library)
|
109
113
|
end
|
110
114
|
self.classes.each do |klass|
|
111
115
|
klass.send(:build_structure)
|
@@ -137,13 +141,6 @@ module Rod
|
|
137
141
|
raise DatabaseError.new("Size of data file of #{klass} is invalid: #{file_size}")
|
138
142
|
end
|
139
143
|
set_page_count(klass,file_size / _page_size)
|
140
|
-
if options[:migrate]
|
141
|
-
next unless klass.name =~ LEGACY_RE
|
142
|
-
new_class = klass.name.sub(LEGACY_RE,"").constantize
|
143
|
-
set_count(new_class,meta[:count])
|
144
|
-
pages = (meta[:count] * new_class.struct_size / _page_size.to_f).ceil
|
145
|
-
set_page_count(new_class,pages)
|
146
|
-
end
|
147
144
|
end
|
148
145
|
if metadata_copy.size > 0
|
149
146
|
@handler = nil
|
@@ -151,34 +148,57 @@ module Rod
|
|
151
148
|
metadata_copy.keys.join("\n - "))
|
152
149
|
end
|
153
150
|
_open(@handler)
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
FileUtils.mv(new_file_name,current_file_name)
|
172
|
-
end
|
173
|
-
@classes.delete(klass)
|
174
|
-
new_class.model_path = nil
|
151
|
+
end
|
152
|
+
|
153
|
+
# Migrates the database, which is located at +path+. The
|
154
|
+
# old version of the DB is placed at +path+/backup.
|
155
|
+
def migrate_database(path)
|
156
|
+
raise DatabaseError.new("Database already opened.") if opened?
|
157
|
+
@readonly = false
|
158
|
+
@path = canonicalize_path(path)
|
159
|
+
@metadata = load_metadata
|
160
|
+
create_legacy_classes
|
161
|
+
FileUtils.mkdir_p(@path + BACKUP_PREFIX)
|
162
|
+
Dir.glob(@path + "*").each do |file|
|
163
|
+
# Don't move the directory itself and speciall classes data.
|
164
|
+
unless file.to_s == @path + BACKUP_PREFIX[0..-2] ||
|
165
|
+
special_classes.map{|c| c.path_for_data(@path)}.include?(file.to_s)
|
166
|
+
puts "Moving #{file} to #{@path + BACKUP_PREFIX}" if $ROD_DEBUG
|
167
|
+
FileUtils.mv(file,@path + BACKUP_PREFIX)
|
175
168
|
end
|
176
|
-
close_database(false,true)
|
177
|
-
options.delete(:migrate)
|
178
|
-
readonly = options.delete(:old_readonly)
|
179
|
-
options[:readonly] = readonly
|
180
|
-
open_database(path,options)
|
181
169
|
end
|
170
|
+
remove_files(self.inline_library)
|
171
|
+
self.classes.each do |klass|
|
172
|
+
klass.send(:build_structure)
|
173
|
+
end
|
174
|
+
generate_c_code(@path, self.classes)
|
175
|
+
@handler = _init_handler(@path)
|
176
|
+
self.classes.each do |klass|
|
177
|
+
next unless special_class?(klass) or legacy_class?(klass)
|
178
|
+
meta = @metadata[klass.name]
|
179
|
+
set_count(klass,meta[:count])
|
180
|
+
file_size = File.new(klass.path_for_data(@path)).size
|
181
|
+
unless file_size % _page_size == 0
|
182
|
+
raise DatabaseError.new("Size of data file of #{klass} is invalid: #{file_size}")
|
183
|
+
end
|
184
|
+
set_page_count(klass,file_size / _page_size)
|
185
|
+
next unless legacy_class?(klass)
|
186
|
+
new_class = klass.name.sub(LEGACY_RE,"").constantize
|
187
|
+
set_count(new_class,meta[:count])
|
188
|
+
pages = (meta[:count] * new_class.struct_size / _page_size.to_f).ceil
|
189
|
+
set_page_count(new_class,pages)
|
190
|
+
end
|
191
|
+
_open(@handler)
|
192
|
+
self.classes.each do |klass|
|
193
|
+
next unless legacy_class?(klass)
|
194
|
+
klass.migrate
|
195
|
+
@classes.delete(klass)
|
196
|
+
end
|
197
|
+
path_with_date = @path + BACKUP_PREFIX[0..-2] + "_" +
|
198
|
+
Time.new.strftime("%Y_%m_%d_%H_%M_%S") + "/"
|
199
|
+
puts "Moving #{@path + BACKUP_PREFIX} to #{path_with_date}" if $ROD_DEBUG
|
200
|
+
FileUtils.mv(@path + BACKUP_PREFIX,path_with_date)
|
201
|
+
close_database
|
182
202
|
end
|
183
203
|
|
184
204
|
# Closes the database.
|
@@ -189,7 +209,7 @@ module Rod
|
|
189
209
|
#
|
190
210
|
# If the +skip_indeces+ flat is set to true, the indices are not written.
|
191
211
|
def close_database(purge_classes=false,skip_indices=false)
|
192
|
-
raise DatabaseError.new("Database not opened.")
|
212
|
+
raise DatabaseError.new("Database not opened.") unless opened?
|
193
213
|
|
194
214
|
unless readonly_data?
|
195
215
|
unless referenced_objects.select{|k, v| not v.empty?}.size == 0
|
@@ -228,6 +248,18 @@ module Rod
|
|
228
248
|
@referenced_objects ||= {}
|
229
249
|
end
|
230
250
|
|
251
|
+
# Returns true if the class is one of speciall classes
|
252
|
+
# (JoinElement, PolymorphicJoinElement, StringElement).
|
253
|
+
def special_class?(klass)
|
254
|
+
self.special_classes.include?(klass)
|
255
|
+
end
|
256
|
+
|
257
|
+
# Returns true if the +klass+ is a legacy class, i.e.
|
258
|
+
# a class generated during migration used to access the legacy
|
259
|
+
# data.
|
260
|
+
def legacy_class?(klass)
|
261
|
+
klass.name =~ LEGACY_RE
|
262
|
+
end
|
231
263
|
|
232
264
|
# Adds the +klass+ to the set of classes linked with this database.
|
233
265
|
def add_class(klass)
|
@@ -354,10 +386,11 @@ module Rod
|
|
354
386
|
end
|
355
387
|
end
|
356
388
|
|
389
|
+
|
357
390
|
# Prints the layout of the pages in memory and other
|
358
391
|
# internal data of the model.
|
359
392
|
def print_layout
|
360
|
-
raise DatabaseError.new("Database not opened.")
|
393
|
+
raise DatabaseError.new("Database not opened.") unless opened?
|
361
394
|
_print_layout(@handler)
|
362
395
|
end
|
363
396
|
|
@@ -368,6 +401,21 @@ module Rod
|
|
368
401
|
|
369
402
|
protected
|
370
403
|
|
404
|
+
# Returns the metadata loaded from the database's metadata file.
|
405
|
+
# Raises exception if the version of library and database are
|
406
|
+
# not compatible.
|
407
|
+
def load_metadata
|
408
|
+
metadata = {}
|
409
|
+
File.open(@path + DATABASE_FILE) do |input|
|
410
|
+
metadata = YAML::load(input)
|
411
|
+
end
|
412
|
+
unless valid_version?(metadata["Rod"][:version])
|
413
|
+
raise IncompatibleVersion.new("Incompatible versions - library #{VERSION} vs. " +
|
414
|
+
"file #{metatdata["Rod"][:version]}")
|
415
|
+
end
|
416
|
+
metadata
|
417
|
+
end
|
418
|
+
|
371
419
|
# Checks if the version of the library is valid.
|
372
420
|
# Consult https://github.com/apohllo/rod/wiki for versioning scheme.
|
373
421
|
def valid_version?(version)
|
@@ -404,10 +452,6 @@ module Rod
|
|
404
452
|
result[:readonly] = options
|
405
453
|
when Hash
|
406
454
|
result = options
|
407
|
-
if options[:migrate]
|
408
|
-
result[:old_readonly] = options[:readonly]
|
409
|
-
result[:readonly] = false
|
410
|
-
end
|
411
455
|
else
|
412
456
|
raise RodException.new("Invalid options for open_database: #{options}!")
|
413
457
|
end
|
@@ -475,8 +519,7 @@ module Rod
|
|
475
519
|
end
|
476
520
|
|
477
521
|
# During migration it creats the classes which are used to read
|
478
|
-
# the legacy data.
|
479
|
-
# actual classes not to conflict with paths of legacy data.
|
522
|
+
# the legacy data.
|
480
523
|
def create_legacy_classes
|
481
524
|
legacy_module = nil
|
482
525
|
begin
|
@@ -485,11 +528,11 @@ module Rod
|
|
485
528
|
legacy_module = Module.new
|
486
529
|
Object.const_set(LEGACY_MODULE,legacy_module)
|
487
530
|
end
|
531
|
+
generate_classes(legacy_module)
|
488
532
|
self.classes.each do |klass|
|
489
|
-
next
|
490
|
-
klass.model_path =
|
533
|
+
next unless legacy_class?(klass)
|
534
|
+
klass.model_path = BACKUP_PREFIX + klass.model_path
|
491
535
|
end
|
492
|
-
generate_classes(legacy_module)
|
493
536
|
end
|
494
537
|
|
495
538
|
|
data/lib/rod/constants.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Rod
|
2
|
-
VERSION = "0.
|
2
|
+
VERSION = "0.7.0"
|
3
3
|
|
4
4
|
# The name of file containing the data base.
|
5
5
|
DATABASE_FILE = "database.yml"
|
@@ -35,5 +35,6 @@ module Rod
|
|
35
35
|
NEW_DATA_SUFFIX = ".new"
|
36
36
|
LEGACY_MODULE = "Legacy"
|
37
37
|
LEGACY_RE = /^#{LEGACY_MODULE}::/
|
38
|
+
BACKUP_PREFIX = "backup/"
|
38
39
|
|
39
40
|
end
|
data/lib/rod/database.rb
CHANGED
@@ -12,21 +12,6 @@ module Rod
|
|
12
12
|
# models simultaneously. This is due to the way RubyInline creates and
|
13
13
|
# names (after the name of the class) the C code.
|
14
14
|
class Database < AbstractDatabase
|
15
|
-
# This flag indicates, if Database and Model works in development
|
16
|
-
# mode, i.e. the dynamically loaded library has a unique, different id each time
|
17
|
-
# the rod library is used.
|
18
|
-
@@rod_development_mode = false
|
19
|
-
|
20
|
-
# Writer of the +rod_development_mode+ flag.
|
21
|
-
def self.development_mode=(value)
|
22
|
-
@@rod_development_mode = value
|
23
|
-
end
|
24
|
-
|
25
|
-
# Reader of the +rod_development_mode+ flag.
|
26
|
-
def self.development_mode
|
27
|
-
@@rod_development_mode
|
28
|
-
end
|
29
|
-
|
30
15
|
protected
|
31
16
|
|
32
17
|
## Helper methods printing some generated code ##
|
@@ -145,12 +130,6 @@ module Rod
|
|
145
130
|
str.margin
|
146
131
|
end
|
147
132
|
|
148
|
-
# Returns true if the class is one of speciall classes
|
149
|
-
# (JoinElement, PolymorphicJoinElement, StringElement).
|
150
|
-
def special_class?(klass)
|
151
|
-
self.special_classes.include?(klass)
|
152
|
-
end
|
153
|
-
|
154
133
|
#########################################################################
|
155
134
|
# Implementations of abstract methods
|
156
135
|
#########################################################################
|
@@ -167,6 +146,15 @@ module Rod
|
|
167
146
|
@inline_library
|
168
147
|
end
|
169
148
|
|
149
|
+
# Allocates the space for the +klass+ in the data file.
|
150
|
+
def allocate_space(klass)
|
151
|
+
empty_data = "\0" * _page_size
|
152
|
+
File.open(klass.path_for_data(@path),"w") do |out|
|
153
|
+
send("_#{klass.struct_name}_page_count",@handler).
|
154
|
+
times{|i| out.print(empty_data)}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
170
158
|
|
171
159
|
# Generates the code C responsible for management of the database.
|
172
160
|
def generate_c_code(path, classes)
|
@@ -194,7 +182,6 @@ module Rod
|
|
194
182
|
|
195
183
|
builder.prefix(self.class.rod_exception)
|
196
184
|
|
197
|
-
|
198
185
|
#########################################
|
199
186
|
# Model struct
|
200
187
|
#########################################
|
data/lib/rod/exception.rb
CHANGED
@@ -41,6 +41,10 @@ module Rod
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
# This exception is raised, when the key is not present in the database.
|
45
|
+
class KeyMissing < DatabaseError
|
46
|
+
end
|
47
|
+
|
44
48
|
# This exception is raised if argument for some Rod API call
|
45
49
|
# (such as field, has_one, has_many) is invalid.
|
46
50
|
class InvalidArgument < RodException
|
data/lib/rod/index/base.rb
CHANGED
data/lib/rod/index/flat_index.rb
CHANGED
@@ -0,0 +1,266 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rod/index/base'
|
3
|
+
|
4
|
+
module Rod
|
5
|
+
module Index
|
6
|
+
# This implementation of index is based on the
|
7
|
+
# Berkeley DB Hash access method.
|
8
|
+
class HashIndex < Base
|
9
|
+
# Wrapper class for the database struct.
|
10
|
+
class Handle
|
11
|
+
end
|
12
|
+
|
13
|
+
# Initializes the index with +path+ and +class+.
|
14
|
+
# Options are not (yet) used.
|
15
|
+
def initialize(path,klass,options={})
|
16
|
+
@path = path + ".db"
|
17
|
+
@klass = klass
|
18
|
+
_open(@path,:create => true)
|
19
|
+
@index = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Stores the index on disk.
|
23
|
+
def save
|
24
|
+
@index.each do |key,collection|
|
25
|
+
key = Marshal.dump(key)
|
26
|
+
_put(key,collection.offset,collection.size)
|
27
|
+
end
|
28
|
+
_close()
|
29
|
+
end
|
30
|
+
|
31
|
+
# Clears the contents of the index.
|
32
|
+
def destroy
|
33
|
+
_close()
|
34
|
+
_open(@path,:truncate => true)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Simple iterator.
|
38
|
+
def each
|
39
|
+
if block_given?
|
40
|
+
@index.each do |key,value|
|
41
|
+
yield key,value
|
42
|
+
end
|
43
|
+
_each do |key,value|
|
44
|
+
key = Marshal.load(key)
|
45
|
+
unless @index[key]
|
46
|
+
yield key,self[key]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
else
|
50
|
+
enum_for(:each)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
def get(key)
|
56
|
+
return @index[key] if @index.has_key?(key)
|
57
|
+
begin
|
58
|
+
value = _get(Marshal.dump(key))
|
59
|
+
rescue Rod::KeyMissing => ex
|
60
|
+
value = nil
|
61
|
+
end
|
62
|
+
@index[key] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
def set(key,value)
|
66
|
+
@index[key] = value
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def self.rod_exception
|
71
|
+
str =<<-END
|
72
|
+
|VALUE rodException(){
|
73
|
+
| VALUE klass = rb_const_get(rb_cObject, rb_intern("Rod"));
|
74
|
+
| klass = rb_const_get(klass, rb_intern("DatabaseError"));
|
75
|
+
| return klass;
|
76
|
+
|}
|
77
|
+
END
|
78
|
+
str.margin
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.entry_struct
|
82
|
+
str =<<-END
|
83
|
+
|typedef struct rod_entry {
|
84
|
+
| unsigned long offset;
|
85
|
+
| unsigned long size;
|
86
|
+
|} rod_entry_struct;
|
87
|
+
END
|
88
|
+
str.margin
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.convert_key
|
92
|
+
str =<<-END
|
93
|
+
|DBT _convert_key(VALUE key){
|
94
|
+
| long int_key;
|
95
|
+
| double float_key;
|
96
|
+
| DBT db_key;
|
97
|
+
|
|
98
|
+
| memset(&db_key, 0, sizeof(DBT));
|
99
|
+
| if(rb_obj_is_kind_of(key,rb_cInteger)){
|
100
|
+
| int_key = NUM2LONG(key);
|
101
|
+
| db_key.data = &int_key;
|
102
|
+
| db_key.size = sizeof(long);
|
103
|
+
| } else if(rb_obj_is_kind_of(key,rb_cFloat)){
|
104
|
+
| float_key = NUM2DBL(key);
|
105
|
+
| db_key.data = &float_key;
|
106
|
+
| db_key.size = sizeof(double);
|
107
|
+
| } else {
|
108
|
+
| db_key.data = RSTRING_PTR(key);
|
109
|
+
| db_key.size = RSTRING_LEN(key);
|
110
|
+
| }
|
111
|
+
| // is it legal?
|
112
|
+
| return db_key;
|
113
|
+
|}
|
114
|
+
END
|
115
|
+
str.margin
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.key_missing_exception
|
119
|
+
str =<<-END
|
120
|
+
|VALUE keyMissingException(){
|
121
|
+
| VALUE klass = rb_const_get(rb_cObject, rb_intern("Rod"));
|
122
|
+
| klass = rb_const_get(klass, rb_intern("KeyMissing"));
|
123
|
+
| return klass;
|
124
|
+
|}
|
125
|
+
END
|
126
|
+
str.margin
|
127
|
+
end
|
128
|
+
|
129
|
+
self.inline(:C) do |builder|
|
130
|
+
builder.include '<db.h>'
|
131
|
+
builder.include '<stdio.h>'
|
132
|
+
builder.add_compile_flags '-ldb-4.8'
|
133
|
+
builder.prefix(self.entry_struct)
|
134
|
+
builder.prefix(self.rod_exception)
|
135
|
+
builder.prefix(self.key_missing_exception)
|
136
|
+
builder.prefix(self.convert_key)
|
137
|
+
|
138
|
+
|
139
|
+
str =<<-END
|
140
|
+
|void _open(const char * path, VALUE options){
|
141
|
+
| DB * db_pointer;
|
142
|
+
| u_int32_t flags;
|
143
|
+
| int return_value;
|
144
|
+
| VALUE handleClass;
|
145
|
+
| VALUE handle;
|
146
|
+
| VALUE mod;
|
147
|
+
| db_pointer = ALLOC(DB);
|
148
|
+
| return_value = db_create(&db_pointer,NULL,0);
|
149
|
+
| if(return_value != 0){
|
150
|
+
| rb_raise(rodException(),"%s",db_strerror(return_value));
|
151
|
+
| }
|
152
|
+
|
|
153
|
+
| flags = 0;
|
154
|
+
| if(rb_hash_aref(options,ID2SYM(rb_intern("create"))) == Qtrue){
|
155
|
+
| flags |= DB_CREATE;
|
156
|
+
| }
|
157
|
+
| if(rb_hash_aref(options,ID2SYM(rb_intern("truncate"))) == Qtrue){
|
158
|
+
| flags |= DB_TRUNCATE;
|
159
|
+
| }
|
160
|
+
|
|
161
|
+
| return_value = db_pointer->open(db_pointer,NULL,path,
|
162
|
+
| NULL,DB_HASH,flags,0);
|
163
|
+
| if(return_value != 0){
|
164
|
+
| rb_raise(rodException(),"%s",db_strerror(return_value));
|
165
|
+
| }
|
166
|
+
| mod = rb_const_get(rb_cObject, rb_intern("Rod"));
|
167
|
+
| mod = rb_const_get(mod, rb_intern("Index"));
|
168
|
+
| mod = rb_const_get(mod, rb_intern("HashIndex"));
|
169
|
+
| handleClass = rb_const_get(mod, rb_intern("Handle"));
|
170
|
+
| // TODO the handle memory should be made free
|
171
|
+
| handle = Data_Wrap_Struct(handleClass,0,0,db_pointer);
|
172
|
+
| rb_iv_set(self,"@handle",handle);
|
173
|
+
|}
|
174
|
+
END
|
175
|
+
builder.c(str.margin)
|
176
|
+
|
177
|
+
str =<<-END
|
178
|
+
|void _close(){
|
179
|
+
| VALUE handle;
|
180
|
+
| DB *db_pointer;
|
181
|
+
| handle = rb_iv_get(self,"@handle");
|
182
|
+
| Data_Get_Struct(handle,DB,db_pointer);
|
183
|
+
| if(db_pointer != NULL){
|
184
|
+
| db_pointer->close(db_pointer,0);
|
185
|
+
| rb_iv_set(self,"@handle",Qnil);
|
186
|
+
| } else {
|
187
|
+
| rb_raise(rodException(),"DB handle is NULL\\n");
|
188
|
+
| }
|
189
|
+
|}
|
190
|
+
END
|
191
|
+
builder.c(str.margin)
|
192
|
+
|
193
|
+
str =<<-END
|
194
|
+
|void _each(){
|
195
|
+
|
|
196
|
+
|}
|
197
|
+
END
|
198
|
+
builder.c(str.margin)
|
199
|
+
|
200
|
+
str =<<-END
|
201
|
+
|VALUE _get(VALUE key){
|
202
|
+
| VALUE handle;
|
203
|
+
| DB *db_pointer;
|
204
|
+
| DBT db_key, db_value;
|
205
|
+
| rod_entry_struct entry;
|
206
|
+
| VALUE result;
|
207
|
+
| int return_value;
|
208
|
+
|
|
209
|
+
| handle = rb_iv_get(self,"@handle");
|
210
|
+
| Data_Get_Struct(handle,DB,db_pointer);
|
211
|
+
| if(db_pointer != NULL){
|
212
|
+
| memset(&db_value, 0, sizeof(DBT));
|
213
|
+
| db_key = _convert_key(key);
|
214
|
+
| db_value.data = &entry;
|
215
|
+
| db_value.ulen = sizeof(rod_entry_struct);
|
216
|
+
| db_value.flags = DB_DBT_USERMEM;
|
217
|
+
| return_value = db_pointer->get(db_pointer, NULL, &db_key, &db_value, 0);
|
218
|
+
| if(return_value == DB_NOTFOUND){
|
219
|
+
| rb_raise(keyMissingException(),"%s",db_strerror(return_value));
|
220
|
+
| } else if(return_value != 0){
|
221
|
+
| rb_raise(rodException(),"%s",db_strerror(return_value));
|
222
|
+
| } else {
|
223
|
+
| result = rb_ary_new();
|
224
|
+
| rb_ary_push(result,ULONG2NUM(entry.offset));
|
225
|
+
| rb_ary_push(result,ULONG2NUM(entry.size));
|
226
|
+
| return result;
|
227
|
+
| }
|
228
|
+
| } else {
|
229
|
+
| rb_raise(rodException(),"DB handle is NULL\\n");
|
230
|
+
| }
|
231
|
+
| return Qnil;
|
232
|
+
|}
|
233
|
+
END
|
234
|
+
builder.c(str.margin)
|
235
|
+
|
236
|
+
str =<<-END
|
237
|
+
|void _put(VALUE key,unsigned long offset,unsigned long size){
|
238
|
+
| VALUE handle;
|
239
|
+
| DB *db_pointer;
|
240
|
+
| DBT db_key, db_value;
|
241
|
+
| rod_entry_struct entry;
|
242
|
+
| int return_value;
|
243
|
+
|
|
244
|
+
| handle = rb_iv_get(self,"@handle");
|
245
|
+
| Data_Get_Struct(handle,DB,db_pointer);
|
246
|
+
| memset(&db_value, 0, sizeof(DBT));
|
247
|
+
| entry.offset = offset;
|
248
|
+
| entry.size = size;
|
249
|
+
| db_key = _convert_key(key);
|
250
|
+
| db_value.data = &entry;
|
251
|
+
| db_value.size = sizeof(rod_entry_struct);
|
252
|
+
| if(db_pointer != NULL){
|
253
|
+
| return_value = db_pointer->put(db_pointer, NULL, &db_key, &db_value, 0);
|
254
|
+
| if(return_value != 0){
|
255
|
+
| rb_raise(keyMissingException(),"%s",db_strerror(return_value));
|
256
|
+
| }
|
257
|
+
| } else {
|
258
|
+
| rb_raise(rodException(),"DB handle is NULL\\n");
|
259
|
+
| }
|
260
|
+
|}
|
261
|
+
END
|
262
|
+
builder.c(str.margin)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
data/lib/rod/model.rb
CHANGED
@@ -253,6 +253,20 @@ module Rod
|
|
253
253
|
self.add_to_database
|
254
254
|
end
|
255
255
|
|
256
|
+
# Rebuild the index for given +property+. If the property
|
257
|
+
# doesn't have an index, an exception is raised.
|
258
|
+
def self.rebuild_index(property)
|
259
|
+
if properties[property][:index].nil?
|
260
|
+
raise RodException.new("Property '#{property}' doesn't have an index!")
|
261
|
+
end
|
262
|
+
index_for(property,properties[property]).destroy
|
263
|
+
instance_variable_set("@#{property}_index",nil)
|
264
|
+
index = index_for(property,properties[property])
|
265
|
+
self.each do |object|
|
266
|
+
index[object.send(property)] << object
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
256
270
|
#########################################################################
|
257
271
|
# 'Private' instance methods
|
258
272
|
#########################################################################
|
@@ -455,24 +469,39 @@ module Rod
|
|
455
469
|
|
456
470
|
# Migrates the class to the new model, i.e. it copies all the
|
457
471
|
# values of properties that both belong to the class in the old
|
458
|
-
# and the new model
|
472
|
+
# and the new model; it initializes new properties with default
|
473
|
+
# values and migrates the indices to different implementations.
|
459
474
|
def self.migrate
|
475
|
+
# check if the migration is needed
|
476
|
+
old_metadata = self.metadata
|
477
|
+
old_metadata.merge!({:superclass => old_metadata[:superclass].sub(LEGACY_RE,"")})
|
460
478
|
new_class = self.name.sub(LEGACY_RE,"").constantize
|
461
|
-
|
462
|
-
|
479
|
+
return if new_class.compatible?(old_metadata)
|
480
|
+
database.send(:allocate_space,new_class)
|
481
|
+
|
463
482
|
puts "Migrating #{new_class}" if $ROD_DEBUG
|
483
|
+
# Check for incompatible properties.
|
464
484
|
self.properties.each do |name,options|
|
465
485
|
next unless new_class.properties.keys.include?(name)
|
466
|
-
|
467
|
-
|
468
|
-
|
486
|
+
difference = options_difference(options,new_class.properties[name])
|
487
|
+
difference.delete(:index)
|
488
|
+
# Check if there are some options which we cannot migrate at the
|
489
|
+
# moment.
|
490
|
+
unless difference.empty?
|
469
491
|
raise IncompatibleVersion.
|
470
492
|
new("Incompatible definition of property '#{name}'\n" +
|
471
|
-
"Definition is different in the old and "+
|
493
|
+
"Definition of '#{name}' is different in the old and "+
|
472
494
|
"the new schema for '#{new_class}':\n" +
|
473
|
-
" #{
|
474
|
-
" #{new_class.properties[name]}")
|
495
|
+
" #{difference}")
|
475
496
|
end
|
497
|
+
end
|
498
|
+
# Migrate the objects.
|
499
|
+
# initialize prototype objects
|
500
|
+
old_object = self.new
|
501
|
+
new_object = new_class.new
|
502
|
+
self.properties.each do |name,options|
|
503
|
+
next unless new_class.properties.keys.include?(name)
|
504
|
+
print "- #{name}... " if $ROD_DEBUG
|
476
505
|
if self.field?(name)
|
477
506
|
if self.string_field?(options[:type])
|
478
507
|
self.count.times do |position|
|
@@ -508,6 +537,43 @@ module Rod
|
|
508
537
|
end
|
509
538
|
puts "done" if $ROD_DEBUG
|
510
539
|
end
|
540
|
+
# Migrate the indices.
|
541
|
+
new_class.indexed_properties.each do |name,options|
|
542
|
+
# Migrate to new options.
|
543
|
+
old_index_type = self.properties[name] && self.properties[name][:index]
|
544
|
+
if old_index_type.nil?
|
545
|
+
print "- building index #{options[:index]} for '#{name}'... " if $ROD_DEBUG
|
546
|
+
new_class.rebuild_index(name)
|
547
|
+
puts "done" if $ROD_DEBUG
|
548
|
+
else
|
549
|
+
# TODO if index is the same, its file should be copied
|
550
|
+
print "- copying index #{options[:index]} for '#{name}'... " if $ROD_DEBUG
|
551
|
+
new_index = new_class.index_for(name,options)
|
552
|
+
old_index = index_for(name,self.properties[name])
|
553
|
+
new_index.copy(old_index)
|
554
|
+
puts "done" if $ROD_DEBUG
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
# Returns the difference between +options1+ and +options2+.
|
560
|
+
def self.options_difference(options1,options2)
|
561
|
+
old_options = {}
|
562
|
+
options1.each{|k,v| old_options[k] = v.to_s.sub(LEGACY_RE,"")}
|
563
|
+
new_options = {}
|
564
|
+
options2.each{|k,v| new_options[k] = v.to_s}
|
565
|
+
differences = {}
|
566
|
+
old_options.each do |option,value|
|
567
|
+
if new_options[option] != value
|
568
|
+
differences[option] = [value,new_options[option]]
|
569
|
+
end
|
570
|
+
end
|
571
|
+
new_options.each do |option,value|
|
572
|
+
if old_options[option] != value && !differences.has_key?(option)
|
573
|
+
differences[option] = [old_options[option],value]
|
574
|
+
end
|
575
|
+
end
|
576
|
+
differences
|
511
577
|
end
|
512
578
|
|
513
579
|
protected
|
@@ -1100,4 +1166,3 @@ module Rod
|
|
1100
1166
|
end
|
1101
1167
|
end
|
1102
1168
|
end
|
1103
|
-
|
data/lib/rod.rb
CHANGED
data/tests/migration_create.rb
CHANGED
@@ -4,7 +4,7 @@ require File.join(".",File.dirname(__FILE__),"migration_model1")
|
|
4
4
|
|
5
5
|
Rod::Database.development_mode = true
|
6
6
|
|
7
|
-
|
7
|
+
FileUtils.rm_rf("tmp/migration")
|
8
8
|
Database.instance.create_database("tmp/migration")
|
9
9
|
|
10
10
|
count = (ARGV[0] || 10).to_i
|
@@ -19,12 +19,15 @@ count.times do |index|
|
|
19
19
|
:nick => "j#{index}")
|
20
20
|
account.store
|
21
21
|
user1 = User.new(:name => "John#{index}",
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
:surname => "Smith#{index}",
|
23
|
+
:city => "City#{index}",
|
24
|
+
:street => "Street#{index}",
|
25
|
+
:number => index,
|
26
|
+
:account => account,
|
27
|
+
:mother => users[index-1],
|
28
|
+
:father => users[index-2],
|
29
|
+
:friends => [users[index-3],users[index-4]],
|
30
|
+
:files => [files[index],files[index + 1],files[index + 2]])
|
28
31
|
user1.store
|
29
32
|
|
30
33
|
account = Account.new(:login => "amanda#{index}",
|
@@ -32,6 +35,9 @@ count.times do |index|
|
|
32
35
|
account.store
|
33
36
|
user2 = User.new(:name => "Amanda#{index}",
|
34
37
|
:surname => "Amanda#{index}",
|
38
|
+
:city => "Bigcity#{index}",
|
39
|
+
:street => "Small street#{index}",
|
40
|
+
:number => index,
|
35
41
|
:account => account,
|
36
42
|
:mother => users[index-1],
|
37
43
|
:father => users[index-2],
|
data/tests/migration_migrate.rb
CHANGED
@@ -6,8 +6,9 @@ require 'rspec/expectations'
|
|
6
6
|
#$ROD_DEBUG = true
|
7
7
|
Rod::Database.development_mode = true
|
8
8
|
|
9
|
-
Database.instance.
|
10
|
-
|
9
|
+
Database.instance.migrate_database("tmp/migration")
|
10
|
+
Database.instance.open_database("tmp/migration", :readonly => false)
|
11
|
+
Dir.glob("tmp/migration/#{Rod::BACKUP_PREFIX[0..-2]}*").to_a.size.should == 1
|
11
12
|
|
12
13
|
count = (ARGV[0] || 10).to_i
|
13
14
|
count.times do |index|
|
@@ -19,6 +20,7 @@ count.times do |index|
|
|
19
20
|
file.store
|
20
21
|
user = User[index*2]
|
21
22
|
user.age = index
|
23
|
+
user.city = "Small town#{index}"
|
22
24
|
user.file = file
|
23
25
|
user.accounts << account1
|
24
26
|
user.store
|
@@ -35,5 +37,3 @@ count.times do |index|
|
|
35
37
|
end
|
36
38
|
|
37
39
|
Database.instance.close_database
|
38
|
-
|
39
|
-
test(?f,"tmp/migration/#{Rod::DATABASE_FILE}#{Rod::LEGACY_DATA_SUFFIX}").should == true
|
data/tests/migration_model1.rb
CHANGED
@@ -10,6 +10,9 @@ end
|
|
10
10
|
class User < Model
|
11
11
|
field :name, :string, :index => :flat
|
12
12
|
field :surname, :string
|
13
|
+
field :city, :string, :index => :flat
|
14
|
+
field :street, :string, :index => :flat
|
15
|
+
field :number, :integer, :index => :flat
|
13
16
|
has_one :account, :index => :flat
|
14
17
|
has_one :mother, :class_name => "User"
|
15
18
|
has_one :father, :class_name => "User"
|
data/tests/migration_model2.rb
CHANGED
@@ -14,6 +14,15 @@ class User < Model
|
|
14
14
|
# removed
|
15
15
|
# field :surname, :string
|
16
16
|
|
17
|
+
# changed: index flat -> segmented
|
18
|
+
field :city, :string, :index => :segmented
|
19
|
+
|
20
|
+
# changed: index flat -> hash
|
21
|
+
field :street, :string, :index => :hash
|
22
|
+
|
23
|
+
# changed: index flat -> nil
|
24
|
+
field :number, :integer
|
25
|
+
|
17
26
|
# added
|
18
27
|
field :age, :integer
|
19
28
|
|
@@ -40,8 +49,8 @@ class User < Model
|
|
40
49
|
end
|
41
50
|
|
42
51
|
class Account < Model
|
43
|
-
#
|
44
|
-
field :login, :string
|
52
|
+
# changed: index added
|
53
|
+
field :login, :string, :index => :flat
|
45
54
|
|
46
55
|
# removed
|
47
56
|
# field :nick, :string
|
data/tests/migration_verify.rb
CHANGED
@@ -12,7 +12,15 @@ count.times do |index|
|
|
12
12
|
user1 = User[index*2]
|
13
13
|
user1.should_not == nil
|
14
14
|
user = User.find_by_name("John#{index}")
|
15
|
-
|
15
|
+
user.should == user1
|
16
|
+
users = User.find_all_by_city("City#{index}")
|
17
|
+
users.size.should == 0
|
18
|
+
users = User.find_all_by_city("Small town#{index}")
|
19
|
+
users.size.should == 1
|
20
|
+
users[0].should == user1
|
21
|
+
users = User.find_all_by_street("Street#{index}")
|
22
|
+
users.size.should == 1
|
23
|
+
users[0].should == user1
|
16
24
|
user1.name.should == "John#{index}"
|
17
25
|
user1.age.should == index
|
18
26
|
user1.account.should_not == nil
|
@@ -31,7 +39,11 @@ count.times do |index|
|
|
31
39
|
user2 = User[index*2+1]
|
32
40
|
user2.should_not == nil
|
33
41
|
user = User.find_by_name("Amanda#{index}")
|
34
|
-
|
42
|
+
user.should == user2
|
43
|
+
user = User.find_by_city("Bigcity#{index}")
|
44
|
+
#user.should == user2
|
45
|
+
user = User.find_by_street("Small street#{index}")
|
46
|
+
user.should == user2
|
35
47
|
user2.name.should == "Amanda#{index}"
|
36
48
|
user2.age.should == index * 2
|
37
49
|
user2.account.should_not == nil
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: rod
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.7.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Aleksander Pohl
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-09-
|
13
|
+
date: 2011-09-12 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: RubyInline
|
@@ -143,6 +143,7 @@ files:
|
|
143
143
|
- features/collection.feature
|
144
144
|
- features/collection_proxy.feature
|
145
145
|
- features/flat_indexing.feature
|
146
|
+
- features/hash_indexing.feature
|
146
147
|
- features/inheritence.feature
|
147
148
|
- features/muliple_db.feature
|
148
149
|
- features/persistence.feature
|
@@ -165,6 +166,7 @@ files:
|
|
165
166
|
- lib/rod/exception.rb
|
166
167
|
- lib/rod/index/base.rb
|
167
168
|
- lib/rod/index/flat_index.rb
|
169
|
+
- lib/rod/index/hash_index.rb
|
168
170
|
- lib/rod/index/segmented_index.rb
|
169
171
|
- lib/rod/join_element.rb
|
170
172
|
- lib/rod/model.rb
|
@@ -237,6 +239,7 @@ test_files:
|
|
237
239
|
- features/collection.feature
|
238
240
|
- features/collection_proxy.feature
|
239
241
|
- features/flat_indexing.feature
|
242
|
+
- features/hash_indexing.feature
|
240
243
|
- features/inheritence.feature
|
241
244
|
- features/muliple_db.feature
|
242
245
|
- features/persistence.feature
|