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.db}/#{@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. # WARNING - NO UNIT TEST EXIST YET! # CONSIDER - Deprecation coming soon... Should use the 'find_extended' method instead. 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) @pos_buffer=Btrieve.create_string_buffer(POS_BLOCK_SIZE) 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 # Packs a value according to the metadata of a particular column. def pack_value(column, value) packer_string = column[:unpacker] match = packer_string.match(/@(\d+)([a-zA-Z]+)(\d*)/) datatype = match[2] size = match[3].empty? ? BtrieveSchema::SIZEOF[datatype] : BtrieveSchema::SIZEOF[datatype]*match[3].to_i packer = "#{datatype}#{size}" array = [value] array.pack(packer) end # Returns an array of hashes that match the criteria defined by the filter parameter. # An example of the filter parameter is given below - # # filter = [[:dept_name, :equal, 'Philosophy'], :and, [:name, :not_equal, 'PHI 101'], :and, [:name, :not_equal, :dept_name]] # # The following COMPARISON operators are supported in the logical comparison clauses - # :equal # :greater_than # :less_than # :not_equal # :greater_than_or_equal # :less_than_or_equal # # Logical comparison clauses can chained together using the following boolean operators - # :and # :or # # NOTE - The logical comparisons can compare a columns to a constant OR to another column in the same record. def find_extended(index, filter, fields_to_return, records_to_fetch) record_size = @schema[:record_size] records_buffer = record_size*records_to_fetch max_reject_count = 65535 start_point = :next_record searcher = BtrieveSearchBuffer.new(self, filter, fields_to_return, records_to_fetch, max_reject_count, max_reject_count) search_buffer = searcher.get_encoded_buffer buff_diff = records_buffer - search_buffer.size search_buffer << Btrieve.create_string_buffer(buff_diff) if(buff_diff > 0) tmp_buff = Btrieve.create_string_buffer(record_size) key_buff = Btrieve.create_string_buffer(record_size) btr_op(GET_FIRST, @pos_buffer, tmp_buff, key_buff, index, [OK]) ops_result = btr_op(GET_NEXT_EXTENDED, @pos_buffer, search_buffer, key_buff, index, [OK, EOF]) num_records = search_buffer.unpack('S')[0] record_byte_pointer=2 records = [] (0...num_records).each do |index| record_length = search_buffer.unpack("@#{record_byte_pointer}S")[0] record_position = search_buffer.unpack("@#{2+record_byte_pointer}i")[0] record=search_buffer.unpack("@#{6+record_byte_pointer}a#{record_length}")[0] record_byte_pointer += 6+record_length unpacker = fields_to_return.inject(["", 0]) do |arr, c| column = @schema[:columns][c] str = BtrieveSchema.unpacker(arr[1], column[:datatype], column[:size]) arr[0] << str arr[1] += column[:size] arr end fields = record.unpack(unpacker[0]) records << {:position=>record_position, :values=>Hash[*fields_to_return.zip(fields).flatten]} end records 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) open btr_op(GET_EQUAL, @pos_buffer, btr_record.data_buffer, key_buffer, index, [KEY_NOT_FOUND, OK, EOF]) btr_record = btr_record.nil? ? nil : btr_record.set_physical_position close 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.set_physical_position 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