require 'fileutils' require 'yaml' #require 'filelock' require 'pstore' require 'pp' #* list of public methods #- initialize( path_to_filename ) # #- self.create_table( path_to_basename ) #--- method to create new table #--- ex. NERA::Database.create_table( "./db") # #- add( new_rec ) #--- add a new record to the table. #--- new_rec must be specified by a Hash. #--- ex. db.add( { :key1 => 'test', :key2 => DateTime.now, ... } ) #--- You don't need to specify the ':id' key. If it is specified, it is omitted. #--- During the transaction, files are locked with exclusive locks. #--- return true if the addition succeeded. #--- raise RuntimeError if the specified hash is not valid. # #- find_by_id( id ) #--- returns records in Hash. #--- id can be Integer, Array of Integers, or Range of Integers. #--- During reading file, the files are locked with shared locks. #--- If there are several records, it returns Array of Hash. # #- find_all { |rec| (condition) } #--- returns the array of the matched records. #--- return nil if no record matched. # #- update( rec ) #--- update a record #--- ex. db.update( {:id=>3, :key1=>'updated', :key2=>DateTime.now} ) #--- returns nil if the record of the specified 'id' does not exist. # #- destroy( id ) #--- destroy records #--- id may be an Integer, Array of Integers, Range #--- if the specified record is not found in the table, return nil. # #- transaction { ..(transaction).. } #--- all the database files are exclusively locked during block is processed. #--- database files are unlocked at the end of the block. terminates. #--- sample ----------- # # db.transaction do # arr = db.find_all do |record| # record[:id] > 5 # end # # arr.each do |rec| rec[:state] = finished end # db.update( arr) # end # #--- end of sample --- # # # module NERA class Database # name of the file @filename # true if files are exclusively locked @in_transaction # PStore object @pstore # search a data file in this order # classes of values VALUE_CLASSES = [ String, Symbol, Integer, Float, Date, DateTime, TrueClass, FalseClass, NilClass ] public # ---------------------------------- def initialize( path_to_file ) @filename = path_to_file @in_transaction = nil unless File.exist?(@filename) raise "No such database : #{@filename}" end end # ------------------------------------ def transaction if @in_transaction # nothing happens when the process is already in transaction yield return nil end @pstore = PStore.new(@filename) @pstore.transaction( false) do begin @in_transaction = true yield ensure @in_transaction = false nil end end end private # ------------------------------------ def readonly_transaction if @in_transaction yield return nil end @pstore = PStore.new(@filename) @pstore.transaction(true) do yield end end public # ------------------------------------ def self.create_table( filename) if File.exists?( filename) $stderr.puts "Database #{filename} already exists." return nil end db = PStore.new( filename) db.transaction(false) do db[:max_id] = 0 end return true end public # ------------------------------------ def add( new_rec ) unless valid_as_a_record?( new_rec) raise ArgumentError, "This record is not valid." end new_id = nil transaction do new_id = @pstore[:max_id] + 1 new_rec[:id] = new_id @pstore[:max_id] = new_id @pstore[new_id] = new_rec end return new_id end private # ------------------------------------------ def valid_as_a_record?( rec) # check validity raise "rec must be a Hash" unless rec.is_a?(Hash) rec.each do |key,val| unless VALUE_CLASSES.find do |type| val.is_a?(type) end $stderr.puts "Types of this record is not valid : #{key}, #{val}, #{val.class.to_s}" return false end end return true end public # --- find methods -------------------------- def find_by_id( ids) if ids.is_a?(Integer) readonly_transaction do return @pstore[ids] end elsif ids.is_a?(Array) found = [] readonly_transaction do # find all ---- ids.each do |id| found << @pstore[id] end return found end elsif ids.is_a?(Range) found = [] readonly_transaction do matched_ids = @pstore.roots.find_all do |id| ids.include?(id) end return nil unless matched_ids matched_ids.sort! do |id1,id2| id1 <=> id2 end matched_ids.each do |id| found << @pstore[id] end found = nil if found.size == 0 return found end else raise ArgumentError, "Argument of the find method must be Integer, Array or Range. : #{ids.class}" end end public # --- find all --- # usage: find_all { ..(condition).. } def find_all matched = [] readonly_transaction do all_ids = @pstore.roots all_ids.delete( :max_id) all_ids.each do |id| if yield @pstore[id].dup matched << @pstore[id] end end end matched.sort! do |rec1,rec2| rec1[:id] <=> rec2[:id] end matched = nil if matched.size == 0 return matched end public # --- update method --- #- update( rec ) #--- update a record #--- ex. db.update( {:id=>3, :key1=>'updated', :key2=>DateTime.now} ) #--- returns nil if the record of the specified 'id' does not exist. def update( rec) unless valid_as_a_record?( rec) $stderr.puts "This record is not valid." return nil end id_to_update = rec[:id] flag = nil transaction do unless @pstore.root?( id_to_update) return nil end @pstore[id_to_update] = rec flag = true end return flag end public #- destroy( id ) #--- destroy records #--- id may be an Integer, Array of Integers, Range #--- if the specified record is not found in the table, return nil. def destroy( ids) if ids.is_a?( Array) status = [] transaction do ids.each do |id| status << destroy( id) end end return status elsif ids.is_a?( Range) flag = nil transaction do id_array = @pstore.roots.find_all do |id| ids.include?(id) end flag = destroy( id_array) end flag = flag.find do |f| f end return flag elsif ids.is_a?( Integer) stat = nil transaction do stat = @pstore.delete(ids) end return stat else raise ArgumentError, "argument must be specified by Integer, Array of Integer, or Range. : #{ids.class}" end return true end end # ------ end of Database class -------- end # ------ end of NERA module --------