rod 0.6.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +26 -22
- data/Rakefile +18 -4
- data/changelog.txt +33 -0
- data/lib/rod.rb +2 -1
- data/lib/rod/abstract_database.rb +267 -74
- data/lib/rod/abstract_model.rb +66 -0
- data/lib/rod/cache.rb +81 -0
- data/lib/rod/collection_proxy.rb +43 -14
- data/lib/rod/constants.rb +8 -1
- data/lib/rod/database.rb +15 -7
- data/lib/rod/exception.rb +20 -0
- data/lib/rod/join_element.rb +3 -21
- data/lib/rod/model.rb +219 -57
- data/lib/rod/string_element.rb +3 -27
- data/rod.gemspec +0 -1
- metadata +10 -19
data/README
CHANGED
@@ -10,17 +10,25 @@ fast access for data, which rarely changes.
|
|
10
10
|
== FEATURES/PROBLEMS:
|
11
11
|
|
12
12
|
* nice Ruby interface which mimicks Active Record
|
13
|
-
* Ruby-to-C on-the-fly translation based on mmap
|
14
|
-
* optimized for speed
|
13
|
+
* Ruby-to-C on-the-fly translation based on mmap and RubyInline
|
14
|
+
* optimized for (reading) speed
|
15
15
|
* weak reference collections for easy memory reclaims
|
16
|
-
* segmented indices for short start-up
|
16
|
+
* segmented indices for short start-up times
|
17
|
+
* compatibility check of library version
|
18
|
+
* compatibility check of data model
|
19
|
+
* autogeneration of model (based on the database metadata)
|
20
|
+
* automatic model migrations (addition/removal of properties so far)
|
21
|
+
* append of the database (new objects, new elements in plural associations)
|
22
|
+
* databases interlinking (via direct links or inverted indices)
|
17
23
|
|
18
24
|
* doesn't work on Windows
|
25
|
+
* some space is wasted during when database is appended
|
19
26
|
|
20
27
|
== SYNOPSIS:
|
21
28
|
|
22
29
|
ROD is designed for storing and accessing data which rarely changes.
|
23
|
-
It is an opposite of RDBMS as the data is not normalized
|
30
|
+
It is an opposite of RDBMS as the data is not normalized, while
|
31
|
+
"joins" are much faster.
|
24
32
|
It is an opposite of in-memory databases, since it is designed to cover
|
25
33
|
out of core data sets (10 GB and more).
|
26
34
|
|
@@ -30,17 +38,15 @@ is interconnected in many ways, thus the relational model (joins) introduces
|
|
30
38
|
unacceptable performance hit. The size of corpora forces them to be kept
|
31
39
|
on disks. The in-memory data bases are unacceptable for larg corpora and
|
32
40
|
would require the data to be kept mostly in the operational memory,
|
33
|
-
which is not needed, while accessing dictionaries (in most cases only a
|
34
|
-
of the data is
|
35
|
-
number of disk reads was designed. The Ruby interface facilitates it's
|
36
|
-
usage.
|
41
|
+
which is not needed, while accessing dictionaries (in most cases only a fraction
|
42
|
+
of the data is used at the same time). That's why a storage facility which minimizes the
|
43
|
+
number of disk reads was designed. The Ruby interface facilitates it's usage.
|
37
44
|
|
38
45
|
== REQUIREMENTS:
|
39
46
|
|
40
47
|
* RubyInline
|
41
48
|
* english
|
42
49
|
* ActiveModel
|
43
|
-
* weak_hash
|
44
50
|
|
45
51
|
== INSTALL
|
46
52
|
|
@@ -77,23 +83,21 @@ Grab from rubygems:
|
|
77
83
|
end
|
78
84
|
|
79
85
|
MyDatabase.create_database("data")
|
80
|
-
user = User.new
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
account.password = "password"
|
88
|
-
file1 = File.new
|
89
|
-
file1.title = "Lady Gaga video"
|
86
|
+
user = User.new(:name => 'Fred',
|
87
|
+
:surname => 'Smith',
|
88
|
+
:age => 22)
|
89
|
+
account = Account.new(:email => "fred@smith.org",
|
90
|
+
:login => "fred",
|
91
|
+
:password => "password")
|
92
|
+
file1 = File.new(:title => "Lady Gaga video")
|
90
93
|
file2.data = "0012220001..."
|
91
|
-
file2 = File.new
|
92
|
-
file2.title = "Pink Floyd video"
|
94
|
+
file2 = File.new(:title => "Pink Floyd video")
|
93
95
|
file2.data = "0012220001..."
|
96
|
+
|
94
97
|
user.account = account
|
95
98
|
user.files << file1
|
96
99
|
user.files << file2
|
100
|
+
|
97
101
|
user.store
|
98
102
|
account.store
|
99
103
|
file1.store
|
@@ -118,7 +122,7 @@ Grab from rubygems:
|
|
118
122
|
|
119
123
|
(The MIT License)
|
120
124
|
|
121
|
-
Copyright (c) 2008-
|
125
|
+
Copyright (c) 2008-2011 Aleksander Pohl
|
122
126
|
|
123
127
|
Permission is hereby granted, free of charge, to any person obtaining
|
124
128
|
a copy of this software and associated documentation files (the
|
data/Rakefile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
$:.unshift "lib"
|
2
2
|
require 'rod/constants'
|
3
3
|
|
4
|
-
task :default => [:
|
4
|
+
task :default => [:all_tests]
|
5
5
|
|
6
6
|
$gem_name = "rod"
|
7
7
|
|
@@ -22,7 +22,7 @@ task :uninstall do
|
|
22
22
|
sh "sudo gem uninstall #$gem_name"
|
23
23
|
end
|
24
24
|
|
25
|
-
task :all_tests => [:test,:spec
|
25
|
+
task :all_tests => [:test,:regression_test,:spec] do
|
26
26
|
end
|
27
27
|
|
28
28
|
desc "Run performence tests"
|
@@ -36,9 +36,18 @@ desc "Run tests and specs"
|
|
36
36
|
task :test do
|
37
37
|
sh "ruby tests/save_struct.rb"
|
38
38
|
sh "ruby tests/load_struct.rb"
|
39
|
+
sh "ruby tests/class_compatibility_create.rb"
|
40
|
+
sh "ruby tests/class_compatibility_verify.rb"
|
41
|
+
sh "ruby tests/generate_classes_create.rb"
|
42
|
+
sh "ruby tests/generate_classes_rewrite.rb"
|
43
|
+
sh "ruby tests/generate_classes_rewrite.rb"
|
44
|
+
sh "ruby tests/generate_classes_verify.rb"
|
45
|
+
sh "ruby tests/migration_create.rb"
|
46
|
+
sh "ruby tests/migration_migrate.rb"
|
47
|
+
sh "ruby tests/migration_verify.rb"
|
39
48
|
sh "ruby tests/unit/model.rb"
|
40
49
|
sh "ruby tests/unit/model_tests.rb"
|
41
|
-
sh "ruby tests/unit/
|
50
|
+
sh "ruby tests/unit/database.rb"
|
42
51
|
end
|
43
52
|
|
44
53
|
# Should be removed some time -- specs should cover all these cases
|
@@ -48,7 +57,12 @@ task :regression_test do
|
|
48
57
|
end
|
49
58
|
|
50
59
|
task :spec do
|
51
|
-
sh "bundle exec cucumber features/*"
|
60
|
+
sh "bundle exec cucumber --tags ~@ignore features/*"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Work in progress
|
64
|
+
task :wip do
|
65
|
+
sh "bundle exec cucumber --tags @wip features/*"
|
52
66
|
end
|
53
67
|
|
54
68
|
desc "Clean"
|
data/changelog.txt
CHANGED
@@ -1,3 +1,36 @@
|
|
1
|
+
0.6.1
|
2
|
+
- Change order of tests in all_tests
|
3
|
+
- Update DB 'created_at' if empty
|
4
|
+
- #112 fix: indices are rewritten during migration
|
5
|
+
- #110 bulk allocate for objects durign migration
|
6
|
+
- #111 back-up of database.yml during migration
|
7
|
+
- Fix: uninitilized module name
|
8
|
+
- Make incompatible model version message more inteligible
|
9
|
+
- Make index rewrite/convert option more meaningful
|
10
|
+
- #108 fix: indices are destroyed for read/write mode
|
11
|
+
- #81 automatic schema transformation
|
12
|
+
- #104 remove ruby inline generated files when model is generated
|
13
|
+
- tests/read_on_create.rb remove ruby inline and data files during creation
|
14
|
+
- #100 fix: removal of RubyInline generated files
|
15
|
+
- #43 generate classes from metadata
|
16
|
+
- #43 generate classes without module embedding
|
17
|
+
- #96 fix: find_by for assocs
|
18
|
+
- #95 remove weak_hash dependency
|
19
|
+
- #25 check compatibility of class structure
|
20
|
+
- #87 store supreclass of a class in metadata
|
21
|
+
- #26 check version of library: fix revision check - Add ~@ignore tag as default for specs
|
22
|
+
- #93 fix: storage of has_many appended objects - Turn on full object referential integrity
|
23
|
+
- #65 store DB creation and update time in metadata
|
24
|
+
- #89 Replace SimpleWeakHash with Cache
|
25
|
+
- #90 Remove blocks from collection proxy
|
26
|
+
- #80 tests and proper fix for segfault on _allocate_polymorphic
|
27
|
+
- Add check for index of already stored object
|
28
|
+
- #73 append of has many relationship
|
29
|
+
- #82 index (flat -> segmented) transformation tool
|
30
|
+
- #86 Cache indexed properties (optimiz.)
|
31
|
+
- Remove legacy examples
|
32
|
+
- #80 sefault on _allocate_polymorphic_join_elements
|
33
|
+
- #79 fix: reading indices during DB creation causes an error
|
1
34
|
0.6.0
|
2
35
|
- #64 index for associations
|
3
36
|
- Update legacy test to new API
|
data/lib/rod.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'inline'
|
2
2
|
require 'english/inflect'
|
3
|
-
require 'simple_weak_hash'
|
4
3
|
require 'active_model'
|
5
4
|
require 'active_support/dependencies'
|
6
5
|
|
@@ -10,10 +9,12 @@ require 'active_support/dependencies'
|
|
10
9
|
ActiveSupport::Dependencies.mechanism = :require
|
11
10
|
|
12
11
|
require 'rod/abstract_database'
|
12
|
+
require 'rod/abstract_model'
|
13
13
|
require 'rod/constants'
|
14
14
|
require 'rod/database'
|
15
15
|
require 'rod/exception'
|
16
16
|
require 'rod/join_element'
|
17
|
+
require 'rod/cache'
|
17
18
|
require 'rod/collection_proxy'
|
18
19
|
require 'rod/model'
|
19
20
|
require 'rod/string_element'
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
require 'yaml'
|
3
3
|
require 'rod/segmented_index'
|
4
|
+
require 'fileutils'
|
4
5
|
|
5
6
|
module Rod
|
6
7
|
# This class implements the database abstraction, i.e. it
|
@@ -12,9 +13,12 @@ module Rod
|
|
12
13
|
# a given model (set of classes).
|
13
14
|
include Singleton
|
14
15
|
|
16
|
+
# The meta-data of the DataBase.
|
17
|
+
attr_reader :metadata
|
18
|
+
|
15
19
|
# Initializes the classes linked with this database and the handler.
|
16
20
|
def initialize
|
17
|
-
@classes ||= self.
|
21
|
+
@classes ||= self.special_classes
|
18
22
|
@handler = nil
|
19
23
|
end
|
20
24
|
|
@@ -44,54 +48,127 @@ module Rod
|
|
44
48
|
def create_database(path)
|
45
49
|
raise DatabaseError.new("Database already opened.") unless @handler.nil?
|
46
50
|
@readonly = false
|
47
|
-
self.classes.each{|s| s.send(:build_structure)}
|
48
51
|
@path = canonicalize_path(path)
|
49
|
-
# XXX maybe should be more careful?
|
50
52
|
if File.exist?(@path)
|
51
|
-
|
52
|
-
File.delete(file_name) unless File.directory?(file_name)
|
53
|
-
end
|
53
|
+
remove_file("#{@path}database.yml")
|
54
54
|
else
|
55
55
|
Dir.mkdir(@path)
|
56
56
|
end
|
57
|
+
self.classes.each do |klass|
|
58
|
+
klass.send(:build_structure)
|
59
|
+
remove_file(klass.path_for_data(@path))
|
60
|
+
klass.indexed_properties.each do |property,options|
|
61
|
+
path = klass.path_for_index(@path,property,options)
|
62
|
+
if test(?d,path)
|
63
|
+
remove_files(path + "*")
|
64
|
+
elsif test(?f,path)
|
65
|
+
remove_file(path)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
next if special_class?(klass)
|
69
|
+
remove_files_but(klass.inline_library)
|
70
|
+
end
|
57
71
|
generate_c_code(@path, classes)
|
72
|
+
remove_files_but(self.inline_library)
|
73
|
+
@metadata = {}
|
74
|
+
@metadata["Rod"] = {}
|
75
|
+
@metadata["Rod"][:created_at] = Time.now
|
58
76
|
@handler = _init_handler(@path)
|
59
77
|
_create(@handler)
|
60
78
|
end
|
61
79
|
|
62
|
-
# Opens the database at +path+
|
80
|
+
# Opens the database at +path+ with +options+. This allows
|
63
81
|
# for Rod::Model.count, Rod::Model.each, and similar calls.
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
|
82
|
+
# Options:
|
83
|
+
# * +:readonly+ - no modifiaction (append of models and has many association)
|
84
|
+
# is allowed (defaults to +true+)
|
85
|
+
# * +:generate+ - value could be true or a module. If present, generates
|
86
|
+
# the classes from the database metadata. If module given, the classes
|
87
|
+
# are generated withing the module.
|
88
|
+
def open_database(path,options={:readonly => true})
|
68
89
|
raise DatabaseError.new("Database already opened.") unless @handler.nil?
|
69
|
-
|
70
|
-
|
90
|
+
options = convert_options(options)
|
91
|
+
@readonly = options[:readonly]
|
71
92
|
@path = canonicalize_path(path)
|
72
|
-
|
73
|
-
metadata = {}
|
93
|
+
@metadata = {}
|
74
94
|
File.open(@path + DATABASE_FILE) do |input|
|
75
|
-
metadata = YAML::load(input)
|
95
|
+
@metadata = YAML::load(input)
|
76
96
|
end
|
77
|
-
unless valid_version?(metadata["Rod"][:version])
|
78
|
-
raise
|
97
|
+
unless valid_version?(@metadata["Rod"][:version])
|
98
|
+
raise IncompatibleVersion.new("Incompatible versions - library #{VERSION} vs. " +
|
99
|
+
"file #{metatdata["Rod"][:version]}")
|
79
100
|
end
|
101
|
+
if options[:generate]
|
102
|
+
module_instance = (options[:generate] == true ? Object : options[:generate])
|
103
|
+
generate_classes(module_instance)
|
104
|
+
elsif options[:migrate]
|
105
|
+
create_legacy_classes
|
106
|
+
FileUtils.cp(@path + DATABASE_FILE, @path + DATABASE_FILE + LEGACY_DATA_SUFFIX)
|
107
|
+
end
|
108
|
+
self.classes.each do |klass|
|
109
|
+
klass.send(:build_structure)
|
110
|
+
next if special_class?(klass)
|
111
|
+
if options[:generate] && module_instance != Object
|
112
|
+
remove_files_but(klass.inline_library)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
generate_c_code(@path, self.classes)
|
80
116
|
@handler = _init_handler(@path)
|
81
117
|
self.classes.each do |klass|
|
82
|
-
meta = metadata[klass.name]
|
118
|
+
meta = @metadata[klass.name]
|
83
119
|
if meta.nil?
|
84
120
|
# new class
|
85
121
|
next
|
86
122
|
end
|
123
|
+
unless klass.compatible?(meta,self) || options[:generate] || options[:migrate]
|
124
|
+
raise IncompatibleVersion.
|
125
|
+
new("Incompatible definition of '#{klass.name}' class.\n" +
|
126
|
+
"Database and runtime versions are different:\n" +
|
127
|
+
" #{meta}\n #{klass.metadata(self)}")
|
128
|
+
end
|
87
129
|
set_count(klass,meta[:count])
|
88
130
|
file_size = File.new(klass.path_for_data(@path)).size
|
89
131
|
unless file_size % _page_size == 0
|
90
132
|
raise DatabaseError.new("Size of data file of #{klass} is invalid: #{file_size}")
|
91
133
|
end
|
92
134
|
set_page_count(klass,file_size / _page_size)
|
135
|
+
if options[:migrate]
|
136
|
+
next unless klass.name =~ LEGACY_RE
|
137
|
+
new_class = klass.name.sub(LEGACY_RE,"").constantize
|
138
|
+
set_count(new_class,meta[:count])
|
139
|
+
pages = (meta[:count] * new_class.struct_size / _page_size.to_f).ceil
|
140
|
+
set_page_count(new_class,pages)
|
141
|
+
end
|
93
142
|
end
|
94
143
|
_open(@handler)
|
144
|
+
if options[:migrate]
|
145
|
+
empty_data = "\0" * _page_size
|
146
|
+
self.classes.each do |klass|
|
147
|
+
next unless klass.to_s =~ LEGACY_RE
|
148
|
+
new_class = klass.name.sub(LEGACY_RE,"").constantize
|
149
|
+
old_metadata = klass.metadata(self)
|
150
|
+
old_metadata.merge!({:superclass => old_metadata[:superclass].sub(LEGACY_RE,"")})
|
151
|
+
unless new_class.compatible?(old_metadata,self)
|
152
|
+
File.open(new_class.path_for_data(@path),"w") do |out|
|
153
|
+
send("_#{new_class.struct_name}_page_count",@handler).
|
154
|
+
times{|i| out.print(empty_data)}
|
155
|
+
end
|
156
|
+
klass.migrate
|
157
|
+
current_file_name = klass.path_for_data(@path)
|
158
|
+
legacy_file_name = current_file_name + LEGACY_DATA_SUFFIX
|
159
|
+
new_file_name = new_class.path_for_data(@path)
|
160
|
+
FileUtils.mv(current_file_name,legacy_file_name)
|
161
|
+
FileUtils.mv(new_file_name,current_file_name)
|
162
|
+
end
|
163
|
+
@classes.delete(klass)
|
164
|
+
new_class.model_path = nil
|
165
|
+
end
|
166
|
+
close_database(false,true)
|
167
|
+
options.delete(:migrate)
|
168
|
+
readonly = options.delete(:old_readonly)
|
169
|
+
options[:readonly] = readonly
|
170
|
+
open_database(path,options)
|
171
|
+
end
|
95
172
|
end
|
96
173
|
|
97
174
|
# Closes the database.
|
@@ -99,58 +176,36 @@ module Rod
|
|
99
176
|
# If the +purge_classes+ flag is set to true, the information about the classes
|
100
177
|
# linked with this database is removed. This is important for testing, when
|
101
178
|
# classes with same names have different definitions.
|
102
|
-
|
179
|
+
#
|
180
|
+
# If the +skip_indeces+ flat is set to true, the indices are not written.
|
181
|
+
def close_database(purge_classes=false,skip_indices=false)
|
103
182
|
raise DatabaseError.new("Database not opened.") if @handler.nil?
|
104
183
|
|
105
184
|
unless readonly_data?
|
106
185
|
unless referenced_objects.select{|k, v| not v.empty?}.size == 0
|
107
186
|
raise DatabaseError.new("Not all associations have been stored: #{referenced_objects.size} objects")
|
108
187
|
end
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
meta[:count] = count(klass)
|
115
|
-
next if special_class?(klass)
|
116
|
-
# fields
|
117
|
-
fields = meta[:fields] = {} unless klass.fields.empty?
|
118
|
-
klass.fields.each do |field,options|
|
119
|
-
fields[field] = {}
|
120
|
-
fields[field][:options] = options
|
121
|
-
write_index(klass,field,options) if options[:index]
|
122
|
-
end
|
123
|
-
# singular_associations
|
124
|
-
has_one = meta[:has_one] = {} unless klass.singular_associations.empty?
|
125
|
-
klass.singular_associations.each do |name,options|
|
126
|
-
has_one[name] = {}
|
127
|
-
has_one[name][:options] = options
|
128
|
-
write_index(klass,name,options) if options[:index]
|
129
|
-
end
|
130
|
-
# plural_associations
|
131
|
-
has_many = meta[:has_many] = {} unless klass.plural_associations.empty?
|
132
|
-
klass.plural_associations.each do |name,options|
|
133
|
-
has_many[name] = {}
|
134
|
-
has_many[name][:options] = options
|
135
|
-
write_index(klass,name,options) if options[:index]
|
188
|
+
unless skip_indices
|
189
|
+
self.classes.each do |klass|
|
190
|
+
klass.indexed_properties.each do |property,options|
|
191
|
+
write_index(klass,property,options)
|
192
|
+
end
|
136
193
|
end
|
137
194
|
end
|
138
|
-
|
139
|
-
out.puts(YAML::dump(metadata))
|
140
|
-
end
|
195
|
+
write_metadata
|
141
196
|
end
|
142
197
|
_close(@handler)
|
143
198
|
@handler = nil
|
144
199
|
# clear cached data
|
145
200
|
self.clear_cache
|
146
201
|
if purge_classes
|
147
|
-
@classes = self.
|
202
|
+
@classes = self.special_classes
|
148
203
|
end
|
149
204
|
end
|
150
205
|
|
151
206
|
# Clears the cache of the database.
|
152
207
|
def clear_cache
|
153
|
-
classes.each{|c| c.cache.
|
208
|
+
classes.each{|c| c.cache.clear }
|
154
209
|
end
|
155
210
|
|
156
211
|
#########################################################################
|
@@ -264,19 +319,20 @@ module Rod
|
|
264
319
|
def write_index(klass,property,options)
|
265
320
|
raise DatabaseError.new("Readonly database.") if readonly_data?
|
266
321
|
class_index = klass.index_for(property,options)
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
322
|
+
# Only convert the index, without (re)storing the values.
|
323
|
+
unless options[:convert]
|
324
|
+
class_index.each do |key,ids|
|
325
|
+
unless ids.is_a?(CollectionProxy)
|
326
|
+
proxy = CollectionProxy.new(ids[1],self,ids[0],klass)
|
327
|
+
else
|
328
|
+
proxy = ids
|
271
329
|
end
|
272
|
-
|
273
|
-
proxy
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
set_join_element_id(offset, index, rod_id)
|
330
|
+
offset = _allocate_join_elements(proxy.size,@handler)
|
331
|
+
proxy.each_id.with_index do |rod_id,index|
|
332
|
+
set_join_element_id(offset, index, rod_id)
|
333
|
+
end
|
334
|
+
class_index[key] = [offset,proxy.size]
|
278
335
|
end
|
279
|
-
class_index[key] = [offset,proxy.size]
|
280
336
|
end
|
281
337
|
case options[:index]
|
282
338
|
when :flat,true
|
@@ -301,17 +357,21 @@ module Rod
|
|
301
357
|
# Store the object in the database.
|
302
358
|
def store(klass,object)
|
303
359
|
raise DatabaseError.new("Readonly database.") if readonly_data?
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
#
|
308
|
-
object.
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
360
|
+
new_object = (object.rod_id == 0)
|
361
|
+
if new_object
|
362
|
+
send("_store_" + klass.struct_name,object,@handler)
|
363
|
+
# set fields' values
|
364
|
+
object.class.fields.each do |name,options|
|
365
|
+
# rod_id is set during _store
|
366
|
+
object.update_field(name) unless name == "rod_id"
|
367
|
+
end
|
368
|
+
# set ids of objects referenced via singular associations
|
369
|
+
object.class.singular_associations.each do |name,options|
|
370
|
+
object.update_singular_association(name,object.send(name))
|
371
|
+
end
|
313
372
|
end
|
314
373
|
# set ids of objects referenced via plural associations
|
374
|
+
# TODO should be disabled, when there are no new elements
|
315
375
|
object.class.plural_associations.each do |name,options|
|
316
376
|
elements = object.send(name) || []
|
317
377
|
if options[:polymorphic]
|
@@ -345,7 +405,7 @@ module Rod
|
|
345
405
|
library = VERSION.split(".")
|
346
406
|
return false if file[0] != library[0] || file[1] != library[1]
|
347
407
|
if library[1].to_i.even?
|
348
|
-
return
|
408
|
+
return file[2].to_i <= library[2].to_i
|
349
409
|
else
|
350
410
|
return file[2] == library[2]
|
351
411
|
end
|
@@ -362,8 +422,141 @@ module Rod
|
|
362
422
|
end
|
363
423
|
|
364
424
|
# Special classes used by the database.
|
365
|
-
def
|
425
|
+
def special_classes
|
366
426
|
[JoinElement, PolymorphicJoinElement, StringElement]
|
367
427
|
end
|
428
|
+
|
429
|
+
def convert_options(options)
|
430
|
+
result = {}
|
431
|
+
case options
|
432
|
+
when true,false
|
433
|
+
result[:readonly] = options
|
434
|
+
when Hash
|
435
|
+
result = options
|
436
|
+
if options[:migrate]
|
437
|
+
result[:old_readonly] = options[:readonly]
|
438
|
+
result[:readonly] = false
|
439
|
+
end
|
440
|
+
else
|
441
|
+
raise RodException.new("Invalid options for open_database: #{options}!")
|
442
|
+
end
|
443
|
+
result[:readonly] = true if result[:readonly].nil?
|
444
|
+
result
|
445
|
+
end
|
446
|
+
|
447
|
+
# Generates the classes for the data using the metadata from database.yml
|
448
|
+
# +module_instance+ is the module in which the classes are generated.
|
449
|
+
# This allows for embedding them in a separate namespace and use the same model
|
450
|
+
# with different databases in the same time.
|
451
|
+
def generate_classes(module_instance)
|
452
|
+
special_names = special_classes.map{|k| k.name}
|
453
|
+
special_names << "Rod"
|
454
|
+
superclasses = {}
|
455
|
+
@metadata.reject{|k,o| special_names.include?(k)}.each do |k,o|
|
456
|
+
superclasses[k] = o[:superclass]
|
457
|
+
end
|
458
|
+
superclass_tree = {}
|
459
|
+
superclasses.each do |klass,superclass|
|
460
|
+
superclass_tree[klass] = []
|
461
|
+
current_superclass = superclass
|
462
|
+
loop do
|
463
|
+
break if current_superclass.nil?
|
464
|
+
superclass_tree[klass] << current_superclass
|
465
|
+
break if current_superclass == "Rod::Model"
|
466
|
+
current_superclass = superclasses[current_superclass]
|
467
|
+
end
|
468
|
+
end
|
469
|
+
superclasses.keys.sort do |klass1,klass2|
|
470
|
+
if superclass_tree[klass1].include?(klass2)
|
471
|
+
1
|
472
|
+
elsif superclass_tree[klass2].include?(klass1)
|
473
|
+
-1
|
474
|
+
else
|
475
|
+
klass1 <=> klass2
|
476
|
+
end
|
477
|
+
end.each do |klass_name|
|
478
|
+
metadata = @metadata[klass_name]
|
479
|
+
original_name = klass_name
|
480
|
+
if module_instance != Object
|
481
|
+
prefix = module_instance.name + "::"
|
482
|
+
if superclasses.keys.include?(metadata[:superclass])
|
483
|
+
metadata[:superclass] = prefix + metadata[:superclass]
|
484
|
+
end
|
485
|
+
[:fields,:has_one,:has_many].each do |property_type|
|
486
|
+
next if metadata[property_type].nil?
|
487
|
+
metadata[property_type].each do |property,options|
|
488
|
+
if superclasses.keys.include?(options[:options][:class_name])
|
489
|
+
metadata[property_type][property][:options][:class_name] =
|
490
|
+
prefix + options[:options][:class_name]
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
494
|
+
# klass name
|
495
|
+
klass_name = prefix + klass_name
|
496
|
+
@metadata.delete(original_name)
|
497
|
+
@metadata[klass_name] = metadata
|
498
|
+
end
|
499
|
+
klass = Model.generate_class(klass_name,metadata)
|
500
|
+
klass.model_path = Model.struct_name_for(original_name)
|
501
|
+
@classes << klass
|
502
|
+
klass.database_class(self.class)
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
# During migration it creats the classes which are used to read
|
507
|
+
# the legacy data. It also changes the path for the
|
508
|
+
# actual classes not to conflict with paths of legacy data.
|
509
|
+
def create_legacy_classes
|
510
|
+
legacy_module = nil
|
511
|
+
begin
|
512
|
+
legacy_module = Object.const_get(LEGACY_MODULE)
|
513
|
+
rescue NameError
|
514
|
+
legacy_module = Module.new
|
515
|
+
Object.const_set(LEGACY_MODULE,legacy_module)
|
516
|
+
end
|
517
|
+
self.classes.each do |klass|
|
518
|
+
next if special_class?(klass)
|
519
|
+
klass.model_path = Model.struct_name_for(klass.name) + NEW_DATA_SUFFIX
|
520
|
+
end
|
521
|
+
generate_classes(legacy_module)
|
522
|
+
end
|
523
|
+
|
524
|
+
# Removes single file.
|
525
|
+
def remove_file(file_name)
|
526
|
+
if test(?f,file_name)
|
527
|
+
File.delete(file_name)
|
528
|
+
puts "Removing #{file_name}" if $ROD_DEBUG
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Remove all files matching the +pattern+.
|
533
|
+
# If +skip+ given, the file with the given name is not deleted.
|
534
|
+
def remove_files(pattern,skip=nil)
|
535
|
+
Dir.glob(pattern).each do |file_name|
|
536
|
+
remove_file(file_name) unless file_name == skip
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# Removes all files which are similar (i.e. are generated
|
541
|
+
# by RubyInline for the same class) to +name+
|
542
|
+
# excluding the file with exactly the name given.
|
543
|
+
def remove_files_but(name)
|
544
|
+
remove_files(name.sub(INLINE_PATTERN_RE,"*"),name)
|
545
|
+
end
|
546
|
+
|
547
|
+
# Writes the metadata to the database.yml file.
|
548
|
+
def write_metadata
|
549
|
+
metadata = {}
|
550
|
+
rod_data = metadata["Rod"] = {}
|
551
|
+
rod_data[:version] = VERSION
|
552
|
+
rod_data[:created_at] = self.metadata["Rod"][:created_at] || Time.now
|
553
|
+
rod_data[:updated_at] = Time.now
|
554
|
+
self.classes.each do |klass|
|
555
|
+
metadata[klass.name] = klass.metadata(self)
|
556
|
+
end
|
557
|
+
File.open(@path + DATABASE_FILE,"w") do |out|
|
558
|
+
out.puts(YAML::dump(metadata))
|
559
|
+
end
|
560
|
+
end
|
368
561
|
end
|
369
562
|
end
|