rod 0.6.3 → 0.7.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/.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
|