require 'digest/md5' # Represents a single btrieve table for a particular BTR database. class BtrieveTable include Btrieve attr_reader :tablename, :pos_buffer, :schema attr_accessor :primary_key # Initializes a btrieve table. def initialize(tablename, schema=nil) @tablename = tablename @pos_buffer = Btrieve.create_string_buffer(POS_BLOCK_SIZE) @schema = schema ? schema : session.btrieve_schema.get_table_schema(@tablename) end # Opens this btrieve table in preparation for some BTR operation(s). # If a block is passed to the call, the table is closed automatically. # If a block is NOT passed to the call, the client code has to make sure to close the file. def open(mode=NORMAL_MODE, &block) file_unc="//#{session.host}/#{session.data_share}/#{@schema[:filename]}" btr_op(OPEN, @pos_buffer, NULL_BUFFER, file_unc, mode) if block_given? yield close() end end # Finds an array of records in this table, using the specified index number and match hash. # If a block is passed in, the entries of this set are passed into this block one by one. Otherwise, # the set is returned to the caller. def find(match, index_number, &block) allows_duplicates=schema[:index_flags].map{|f|f&1==1;}[index_number] set=nil if(allows_duplicates) set=find_in_index(match, index_number) else rec=find_in_unique_index(match, index_number) set=rec.nil? ? [] : [rec] end if(block_given?) set.each{|record| yield record} else set end end # Finds an array of records in this table, using the specified index number, a key and a range of values. # If a block is passed in, the entries of this set are passed into this block one by one. Otherwise, # the set is returned to the caller. def find_in_range(index_num, key, range, &block) index = @schema[:indices][index_num] columns = @schema[:columns] index_values = index.inject([]){|vals,akey|vals << (akey == key ? range.first : nil); vals} key_packer = index.inject("") do |packer, akey| value_packer = BtrieveSchema.lookup(columns[akey][:datatype], columns[akey][:size])[:unpacker] nil_packer = 'x'*columns[key][:size] packer << (akey == key ? value_packer : nil_packer) packer end key_buffer = index_values.pack(key_packer) btr_record = BtrieveRecord.new(self) records = [] batch do btr_op(GET_GREATER_THAN_OR_EQUAL, @pos_buffer, btr_record.data_buffer, key_buffer, index_num, [OK, EOF]) while(range.include?(btr_record[key])) if(!block.nil?) yield btr_record else records << btr_record end btr_record = BtrieveRecord.new(self) btr_op(GET_NEXT, @pos_buffer, btr_record.data_buffer, key_buffer, index_num, [OK, EOF]) end end if(block.nil?) records end end # Closes this btrieve table to conclude a sequence of BTR operation(s). def close btr_op(CLOSE, @pos_buffer, NULL_BUFFER, NULL_BUFFER, NULL_KEY) end # Loops over the records of this BTR table and passes each record to the block passed in. def each_record(mode=NORMAL_MODE, &block) batch(mode) do while(record = step_next) do yield record end end end # Wraps a sequence of BTR operations between a pair of open and close statements for this table. # DEPRECATED - Use the "open" method instead and pass a block to it. def batch(mode=NORMAL_MODE, &block) open(mode) yield close end # Returns this table's version as a MD5 Digest value. def version() Digest::MD5.hexdigest(BtrieveSchema.sort(schema[:columns]).inject([]){|array,column|array << BtrieveSchema.readable_type(column);array}.to_s) end private def step_next btr_record = BtrieveRecord.new(self) result = btr_op(STEP_NEXT, @pos_buffer, btr_record.data_buffer, NULL_BUFFER, NULL_KEY, [OK, EOF]) return false if(result == EOF) btr_record end def find_matching_index(match) keys = match.keys indices = @schema[:indices] indices.each_index(){ |index| return index if(indices[index].include_all?(keys)) } raise Exception.new("No matching index for #{keys}") end INTEGER_TYPES=['TINYINT','SMALLINT','INTEGER','BIT','UTINYINT','USMALLINT','UINTEGER','UBIGINT'] FLOAT_TYPES=['REAL','DOUBLE'] def finder(match, index_number = nil, &block) index_number ||= find_matching_index(match) index = @schema[:indices][index_number] columns = @schema[:columns] key_components = index.inject([]) do |vals, key| datatype = BtrieveSchema.lookup(columns[key][:datatype], columns[key][:size])[:name] value = match[key] value = value.to_f if FLOAT_TYPES.include?(datatype) value = value.to_i if INTEGER_TYPES.include?(datatype) value = value.ljust(columns[key][:size], ' ') if datatype=='CHAR' vals << value vals end key_packer = index.inject("") do |packer, key| datatype = BtrieveSchema.lookup(columns[key][:datatype], columns[key][:size]) if(match[key] != nil) packer += "#{datatype[:unpacker]}#{datatype[:name].match('CHAR') ? columns[key][:size] : ''}" else packer += 'x'*columns[key][:size] end packer end key_buffer = key_components.pack(key_packer) yield(key_buffer, index_number) end def find_in_unique_index(match, index_number=nil) finder(match, index_number) do |key_buffer, index| btr_record = BtrieveRecord.new(self) batch(){ btr_op(GET_EQUAL, @pos_buffer, btr_record.data_buffer, key_buffer, index, [KEY_NOT_FOUND, OK, EOF]) } btr_record.nil? ? nil : btr_record end end def find_in_index(match, index_number=nil) finder(match, index_number) do |org_key_buffer, index| absolute_match = match.keys.inject({}){|vals, key| vals[key] = match[key] if(match[key] != nil); vals} absolute_match_keys = absolute_match.keys result = [] batch do key_buffer="#{org_key_buffer}" ops_result = btr_op(GET_EQUAL_KEY, @pos_buffer, NULL_BUFFER, key_buffer, index, [OK, EOF, KEY_NOT_FOUND]) ops=GET_EQUAL while(ops_result == OK) btr_record = BtrieveRecord.new(self) ops_result = btr_op(ops, @pos_buffer, btr_record.data_buffer, key_buffer, index, [OK, EOF]) break if(key_buffer!=org_key_buffer) ops=GET_NEXT result << btr_record if(ops_result == OK) end end result end end def session s=BtrieveSession.get_session raise "Cannot manipulate a BtrieveTable when no BtrieveSession exists." if s.nil? s end end