require "file_utils" require "json" class Hash(K,V) def reverse rev = Array(Tuple(K,V)).new keys = Array(K).new each_key do |k| keys << k end keys.reverse.each do |k| rev << {k, self.[k]} end rev end end class DODB::CachedDataBase(V) < DODB::Storage(V) @indexers = [] of Indexer(V) property data = Hash(Int32, V).new def initialize(@directory_name : String) Dir.mkdir_p data_path Dir.mkdir_p locks_directory begin self.last_index rescue self.last_index = -1 end # Load the database in RAM at start-up. DODB::DataBase(V).new(@directory_name).each_with_index do |v, index| puts "\rloading data from #{@directory_name} at index #{index}" self[index] = v end end # Getting data from the hash in RAM. def []?(key : Int32) : V? @data[key] rescue e # FIXME: rescues any error the same way. return nil end # WARNING: data isn't cloned. # You have to do it yourself in case you modify any value, # otherwise you may encounter problems (at least with indexes). def [](key : Int32) : V @data[key] rescue raise MissingEntry.new(key) end def []=(index : Int32, value : V) old_value = self.[index]? check_collisions! index, value, old_value # Removes any old indices or partitions pointing to a value about # to be replaced. if old_value remove_indexes index, old_value end # Avoids corruption in case the application crashes while writing. file_path(index).tap do |path| ::File.write "#{path}.new", value.to_json ::FileUtils.mv "#{path}.new", path end write_partitions index, value if index > last_index self.last_index = index end @data[index] = value end ## # Can be useful for making dumps or to restore a database. def each_with_index(reversed : Bool = false, start_offset = 0, end_offset : Int32? = nil) i = -1 # do not trust key to be the right index (reversed ? @data.reverse : @data).each do |index, v| i += 1 next if start_offset > i break unless end_offset.nil? || i <= end_offset yield v, index end end def delete(key : Int32) value = self[key]? return if value.nil? begin ::File.delete file_path key rescue File::NotFoundError end remove_indexes key, value @data.delete key value end private def remove_data! super @data = Hash(Int32, V).new end end # `DODB::RAMOnlyDataBase` is a database without a file-system representation, # enabling the use of DODB to store data which have the same lifetime as the application. # Indexing (indexes, partitions, tags) will behave the same way. class DODB::RAMOnlyDataBase(V) < DODB::CachedDataBase(V) # Initialization still uses a directory name and creates a few paths. # This is an implementation detail to re-use code of `DODB::Storage` and to get the indexers to work. def initialize(@directory_name : String) Dir.mkdir_p data_path Dir.mkdir_p locks_directory self.last_index = -1 end # WARNING: takes `[]?` and `[]` implementations from `CachedDataBase`. # This will lead to errors in case the implementations change, be aware. def []=(index : Int32, value : V) old_value = self.[index]? check_collisions! index, value, old_value # Removes any old indices or partitions pointing to a value about # to be replaced. if old_value remove_indexes index, old_value end write_partitions index, value if index > last_index self.last_index = index end @data[index] = value end def delete(key : Int32) value = self[key]? return if value.nil? remove_indexes key, value @data.delete key value end private def remove_data! super @data = Hash(Int32, V).new end end