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