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 CHANGED
@@ -1 +1,3 @@
1
1
  rvm: 1.9.2
2
+ before_script:
3
+ - "apt-get install libdb4.6-dev"
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
- * segmented indices for short start-up times
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 so far)
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
- Grab from rubygems:
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 => :flat
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 => :flat
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 => :flat
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
@@ -1,2 +1,3 @@
1
1
  People who contributed to Ruby Object Database:
2
- * qadro
2
+ * Piotr Gurgul
3
+ * Marcin Sieniek
@@ -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
@@ -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 = (index_type == "flat" ? :flat : :segmented)
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 = (index_type == "flat" ? :flat : :segmented)
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.") unless @handler.nil?
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.") unless @handler.nil?
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
- if options[:migrate]
155
- empty_data = "\0" * _page_size
156
- self.classes.each do |klass|
157
- next unless klass.to_s =~ LEGACY_RE
158
- new_class = klass.name.sub(LEGACY_RE,"").constantize
159
- old_metadata = klass.metadata
160
- old_metadata.merge!({:superclass => old_metadata[:superclass].sub(LEGACY_RE,"")})
161
- unless new_class.compatible?(old_metadata)
162
- File.open(new_class.path_for_data(@path),"w") do |out|
163
- send("_#{new_class.struct_name}_page_count",@handler).
164
- times{|i| out.print(empty_data)}
165
- end
166
- klass.migrate
167
- current_file_name = klass.path_for_data(@path)
168
- legacy_file_name = current_file_name + LEGACY_DATA_SUFFIX
169
- new_file_name = new_class.path_for_data(@path)
170
- FileUtils.mv(current_file_name,legacy_file_name)
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.") if @handler.nil?
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.") if @handler.nil?
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. It also changes the path for the
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 if special_class?(klass)
490
- klass.model_path = Model.struct_name_for(klass.name) + NEW_DATA_SUFFIX
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.6.3"
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
@@ -87,6 +87,8 @@ module Rod
87
87
  FlatIndex.new(path,klass,options)
88
88
  when :segmented
89
89
  SegmentedIndex.new(path,klass,options)
90
+ when :hash
91
+ HashIndex.new(path,klass,options)
90
92
  else
91
93
  raise RodException.new("Invalid index type #{type}")
92
94
  end
@@ -18,6 +18,7 @@ module Rod
18
18
  def save
19
19
  File.open(@path,"w") do |out|
20
20
  proxy_index = {}
21
+ #load_index unless loaded?
21
22
  @index.each{|k,col| proxy_index[k] = [col.offset,col.size]}
22
23
  out.puts(Marshal.dump(proxy_index))
23
24
  end
@@ -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
@@ -36,6 +36,7 @@ module Rod
36
36
  # Destroys the index (removes it from the disk completely).
37
37
  def destroy
38
38
  remove_files(@path + "*")
39
+ @buckets = {}
39
40
  end
40
41
 
41
42
  def each
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
- old_object = self.new
462
- new_object = new_class.new
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
- print "- #{name}... " if $ROD_DEBUG
467
- if options.map{|k,v| [k,v.to_s.sub(LEGACY_RE,"")]} !=
468
- new_class.properties[name].map{|k,v| [k,v.to_s]}
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
- " #{options} \n" +
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
@@ -27,4 +27,5 @@ require 'rod/string_element'
27
27
  require 'rod/string_ex'
28
28
  require 'rod/index/base'
29
29
  require 'rod/index/flat_index'
30
+ require 'rod/index/hash_index'
30
31
  require 'rod/index/segmented_index'
@@ -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
- :surname => "Smith#{index}",
23
- :account => account,
24
- :mother => users[index-1],
25
- :father => users[index-2],
26
- :friends => [users[index-3],users[index-4]],
27
- :files => [files[index],files[index + 1],files[index + 2]])
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],
@@ -6,8 +6,9 @@ require 'rspec/expectations'
6
6
  #$ROD_DEBUG = true
7
7
  Rod::Database.development_mode = true
8
8
 
9
- Database.instance.open_database("tmp/migration", :migrate => true,
10
- :readonly => false)
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
@@ -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"
@@ -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
- # present
44
- field :login, :string
52
+ # changed: index added
53
+ field :login, :string, :index => :flat
45
54
 
46
55
  # removed
47
56
  # field :nick, :string
@@ -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
- user1.should == user
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
- user2.should == user
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.6.3
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-09 00:00:00 Z
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