# File lib/kirbybase.rb, line 591
    def build_header_field_string(field_name_def, field_type_def)
        # Put field name at start of string definition.

        temp_field_def = field_name_def.to_s + ':'

        # If field type is a hash, that means that it is not just a

        # simple field.  Either is is a key field, it is being used in an

        # index, it is a default value, it is a required field, it is a

        # Lookup field, it is a Link_many field, or it is a Calculated

        # field.  This next bit of code is to piece together a proper

        # string so that it can be written out to the header rec.

        if field_type_def.is_a?(Hash)
            raise 'Missing :DataType key in field_type hash!' unless \
             field_type_def.has_key?(:DataType)

            temp_type = field_type_def[:DataType]

            raise 'Invalid field type: %s' % temp_type unless \
             KBTable.valid_field_type?(temp_type)

            temp_field_def += field_type_def[:DataType].to_s

            # Check if this field is a key for the table.

            if field_type_def.has_key?(:Key)
                temp_field_def += ':Key->true'
            end
            
            # Check for Index definition.

            if field_type_def.has_key?(:Index)
                raise 'Invalid field type for index: %s' % temp_type \
                 unless KBTable.valid_index_type?(temp_type)    

                temp_field_def += ':Index->' + field_type_def[:Index].to_s
            end
                 
            # Check for Default value definition.

            if field_type_def.has_key?(:Default)
                raise 'Cannot set default value for this type: ' + \
                 '%s' % temp_type unless KBTable.valid_default_type?(
                 temp_type)
                
                unless field_type_def[:Default].nil?
                    raise 'Invalid default value ' + \
                     '%s for column %s' % [field_type_def[:Default],
                     field_name_def] unless KBTable.valid_data_type?(
                     temp_type, field_type_def[:Default])

                    temp_field_def += ':Default->' + \
                     convert_to_encoded_string(temp_type,
                     field_type_def[:Default])
                end    
            end
            
            # Check for Required definition.

            if field_type_def.has_key?(:Required)
                raise 'Required must be true or false!' unless \
                 [true, false].include?(field_type_def[:Required])
                
                temp_field_def += \
                 ':Required->%s' % field_type_def[:Required]
            end
            
            # Check for Lookup field, Link_many field, Calculated field

            # definition.

            if field_type_def.has_key?(:Lookup)
                if field_type_def[:Lookup].is_a?(Array)
                    temp_field_def += \
                     ':Lookup->%s.%s' % field_type_def[:Lookup]
                else
                    tbl = get_table(field_type_def[:Lookup])
                    temp_field_def += \
                     ':Lookup->%s.%s' % [field_type_def[:Lookup],
                     tbl.lookup_key]
                end
            elsif field_type_def.has_key?(:Link_many)
                raise 'Field type for Link_many field must be :ResultSet' \
                 unless temp_type == :ResultSet
                temp_field_def += \
                 ':Link_many->%s=%s.%s' % field_type_def[:Link_many]
            elsif field_type_def.has_key?(:Calculated)
                temp_field_def += \
                 ':Calculated->%s' % field_type_def[:Calculated]
            end
        else
            if KBTable.valid_field_type?(field_type_def)
                temp_field_def += field_type_def.to_s
            elsif table_exists?(field_type_def)
                tbl = get_table(field_type_def)
                temp_field_def += \
                 '%s:Lookup->%s.%s' % [tbl.field_types[
                 tbl.field_names.index(tbl.lookup_key)], field_type_def,
                 tbl.lookup_key]
            else
                raise 'Invalid field type: %s' % field_type_def
            end
        end
        return temp_field_def
    end

    #-----------------------------------------------------------------------

    # rename_table

    #-----------------------------------------------------------------------

    #++

    # Rename a table.

    #

    # *old_tablename*:: Symbol of old table name.

    # *new_tablename*:: Symbol of new table name.

    #

    def rename_table(old_tablename, new_tablename)
        raise "Cannot rename table running in client mode!" if client?
        raise "Table does not exist!" unless table_exists?(old_tablename)
        raise(ArgumentError, 'Existing table name must be a symbol!') \
         unless old_tablename.is_a?(Symbol)
        raise(ArgumentError, 'New table name must be a symbol!') unless \
         new_tablename.is_a?(Symbol)
        raise "Table already exists!" if table_exists?(new_tablename)

        @table_hash.delete(old_tablename)
        @engine.rename_table(old_tablename, new_tablename)
        get_table(new_tablename)
    end

    #-----------------------------------------------------------------------

    # drop_table

    #-----------------------------------------------------------------------

    #++

    # Delete a table.

    #

    # *tablename*:: Symbol of table name.

    #

    def drop_table(tablename)
        raise(ArgumentError, 'Table name must be a symbol!') unless \
         tablename.is_a?(Symbol)
        raise "Table does not exist!" unless table_exists?(tablename)
        @table_hash.delete(tablename)

        return @engine.delete_table(tablename)
    end

    #-----------------------------------------------------------------------

    # table_exists?

    #-----------------------------------------------------------------------

    #++

    # Return true if table exists.

    #

    # *tablename*:: Symbol of table name.

    #

    def table_exists?(tablename)
        raise(ArgumentError, 'Table name must be a symbol!') unless \
         tablename.is_a?(Symbol)

        return @engine.table_exists?(tablename)
    end

end


#---------------------------------------------------------------------------

# KBEngine

#---------------------------------------------------------------------------

class KBEngine
    include DRb::DRbUndumped
    include KBTypeConversionsMixin
    include KBEncryptionMixin

    # Make constructor private.

    private_class_method :new

    #-----------------------------------------------------------------------

    # KBEngine.create_called_from_database_instance

    #-----------------------------------------------------------------------

    def KBEngine.create_called_from_database_instance(db)
        return new(db)
    end

    #-----------------------------------------------------------------------

    # initialize

    #-----------------------------------------------------------------------

    def initialize(db)
        @db = db
        @recno_indexes = {}
        @indexes = {}

        # This hash will hold the table locks if in client/server mode.

        @mutex_hash = {} if @db.server?
    end

    #-----------------------------------------------------------------------

    # init_recno_index

    #-----------------------------------------------------------------------

    def init_recno_index(table)
        return if recno_index_exists?(table)

        with_write_locked_table(table) do |fptr|
            @recno_indexes[table.name] = KBRecnoIndex.new(table)
            @recno_indexes[table.name].rebuild(fptr)
        end
    end

    #-----------------------------------------------------------------------

    # rebuild_recno_index

    #-----------------------------------------------------------------------

    def rebuild_recno_index(table)
        with_write_locked_table(table) do |fptr|
            @recno_indexes[table.name].rebuild(fptr)
        end
    end

    #-----------------------------------------------------------------------

    # remove_recno_index

    #-----------------------------------------------------------------------

    def remove_recno_index(tablename)
        @recno_indexes.delete(tablename)
    end

    #-----------------------------------------------------------------------

    # update_recno_index

    #-----------------------------------------------------------------------

    def update_recno_index(table, recno, fpos)
        @recno_indexes[table.name].update_index_rec(recno, fpos)
    end

    #-----------------------------------------------------------------------

    # recno_index_exists?

    #-----------------------------------------------------------------------

    def recno_index_exists?(table)
        @recno_indexes.include?(table.name)
    end

    #-----------------------------------------------------------------------

    # get_recno_index

    #-----------------------------------------------------------------------

    def get_recno_index(table)
        return @recno_indexes[table.name].get_idx
    end

    #-----------------------------------------------------------------------

    # init_index

    #-----------------------------------------------------------------------

    def init_index(table, index_fields)
        return if index_exists?(table, index_fields)

        with_write_locked_table(table) do |fptr|
            @indexes["#{table.name}_#{index_fields.join('_')}".to_sym] = \
             KBIndex.new(table, index_fields)
            @indexes["#{table.name}_#{index_fields.join('_')}".to_sym
             ].rebuild(fptr)
        end
    end

    #-----------------------------------------------------------------------

    # rebuild_index

    #-----------------------------------------------------------------------

    def rebuild_index(table, index_fields)
        with_write_locked_table(table) do |fptr|
            @indexes["#{table.name}_#{index_fields.join('_')}".to_sym
             ].rebuild(fptr)
        end
    end

    #-----------------------------------------------------------------------

    # remove_indexes

    #-----------------------------------------------------------------------

    def remove_indexes(tablename)
        re_table_name = Regexp.new(tablename.to_s)
        @indexes.delete_if { |k,v| k.to_s =~ re_table_name }
    end

    #-----------------------------------------------------------------------

    # add_to_indexes

    #-----------------------------------------------------------------------

    def add_to_indexes(table, rec, fpos)
        @recno_indexes[table.name].add_index_rec(rec.first, fpos)

        re_table_name = Regexp.new(table.name.to_s)
        @indexes.each_pair do |key, index|
            index.add_index_rec(rec) if key.to_s =~ re_table_name
        end
    end

    #-----------------------------------------------------------------------

    # delete_from_indexes

    #-----------------------------------------------------------------------

    def delete_from_indexes(table, rec, fpos)
        @recno_indexes[table.name].delete_index_rec(rec.recno)

        re_table_name = Regexp.new(table.name.to_s)
        @indexes.each_pair do |key, index|
            index.delete_index_rec(rec.recno) if key.to_s =~ re_table_name
        end
    end

    #-----------------------------------------------------------------------

    # update_to_indexes

    #-----------------------------------------------------------------------

    def update_to_indexes(table, rec)
        re_table_name = Regexp.new(table.name.to_s)
        @indexes.each_pair do |key, index|
            index.update_index_rec(rec) if key.to_s =~ re_table_name
        end
    end

    #-----------------------------------------------------------------------

    # index_exists?

    #-----------------------------------------------------------------------

    def index_exists?(table, index_fields)
        @indexes.include?("#{table.name}_#{index_fields.join('_')}".to_sym)
    end

    #-----------------------------------------------------------------------

    # get_index

    #-----------------------------------------------------------------------

    def get_index(table, index_name)
        return @indexes["#{table.name}_#{index_name}".to_sym].get_idx
    end

    #-----------------------------------------------------------------------

    # get_index_timestamp

    #-----------------------------------------------------------------------

    def get_index_timestamp(table, index_name)
        return @indexes["#{table.name}_#{index_name}".to_sym].get_timestamp
    end

    #-----------------------------------------------------------------------

    # table_exists?

    #-----------------------------------------------------------------------

    def table_exists?(tablename)
        return File.exists?(File.join(@db.path, tablename.to_s + @db.ext))
    end

    #-----------------------------------------------------------------------

    # tables

    #-----------------------------------------------------------------------

    def tables
        list = []
        Dir.foreach(@db.path) { |filename|
            list << File.basename(filename, '.*').to_sym if \
             File.extname(filename) == @db.ext
        }
        return list
    end

    #-----------------------------------------------------------------------

    # new_table

    #-----------------------------------------------------------------------

    #++

    # Create physical file holding table. This table should not be directly

    # called in your application, but only called by #create_table.

    #

    def new_table(name, field_defs, encrypt, record_class)
        # Header rec consists of last record no. used, delete count, and

        # all field names/types.  Here, I am inserting the 'recno' field

        # at the beginning of the fields.

        header_rec = ['000000', '000000', record_class, 'recno:Integer',
         field_defs].join('|')

        header_rec = 'Z' + encrypt_str(header_rec) if encrypt

        begin
            fptr = open(File.join(@db.path, name.to_s + @db.ext), 'w')
            fptr.write(header_rec + "\n")
        ensure
            fptr.close
        end
    end

    #-----------------------------------------------------------------------

    # delete_table

    #-----------------------------------------------------------------------

    def delete_table(tablename)
        with_write_lock(tablename) do
            File.delete(File.join(@db.path, tablename.to_s + @db.ext))
            remove_indexes(tablename)
            remove_recno_index(tablename)
            return true
        end
    end

    #----------------------------------------------------------------------

    # get_total_recs

    #----------------------------------------------------------------------

    def get_total_recs(table)
        return get_recs(table).size
    end

    #-----------------------------------------------------------------------

    # reset_recno_ctr

    #-----------------------------------------------------------------------

    def reset_recno_ctr(table)
        with_write_locked_table(table) do |fptr|
            encrypted, header_line = get_header_record(table, fptr)
            last_rec_no, rest_of_line = header_line.split('|', 2)
            write_header_record(table, fptr, 
             ['%06d' % 0, rest_of_line].join('|'))
            return true
        end
    end

    #-----------------------------------------------------------------------

    # get_header_vars

    #-----------------------------------------------------------------------

    def get_header_vars(table)
        with_table(table) do |fptr|
            encrypted, line = get_header_record(table, fptr)

            last_rec_no, del_ctr, record_class, *flds = line.split('|')
            field_names = flds.collect { |x| x.split(':')[0].to_sym }
            field_types = flds.collect { |x| x.split(':')[1].to_sym }
            field_indexes = [nil] * field_names.size
            field_defaults = [nil] * field_names.size
            field_requireds = [false] * field_names.size
            field_extras = [nil] * field_names.size

            flds.each_with_index do |x,i|
                field_extras[i] = {}
                if x.split(':').size > 2
                    x.split(':')[2..-1].each do |y|
                        if y =~ /Index/
                            field_indexes[i] = y
                        elsif y =~ /Default/
                            field_defaults[i] = \
                             convert_to_native_type(field_types[i],
                              y.split('->')[1])
                        elsif y =~ /Required/
                            field_requireds[i] = \
                             convert_to_native_type(:Boolean,
                              y.split('->')[1])
                        else
                            field_extras[i][y.split('->')[0]] = \
                             y.split('->')[1]
                        end
                    end
                end
            end
            return [encrypted, last_rec_no.to_i, del_ctr.to_i,
             record_class, field_names, field_types, field_indexes,
             field_defaults, field_requireds, field_extras]
        end
    end

    #-----------------------------------------------------------------------

    # get_recs

    #-----------------------------------------------------------------------

    def get_recs(table)
        encrypted = table.encrypted?
        recs = []

        with_table(table) do |fptr|
            begin
                # Skip header rec.

                fptr.readline

                # Loop through table.

                while true
                    # Record current position in table.

                    fpos = fptr.tell
                    rec, line_length = line_to_rec(fptr.readline, encrypted)

                    next if rec.empty?

                    rec << fpos << line_length
                    recs << rec
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end
            return recs
        end
    end

    #-----------------------------------------------------------------------

    # get_recs_by_recno

    #-----------------------------------------------------------------------

    def get_recs_by_recno(table, recnos)
        encrypted = table.encrypted?
        recs = []
        recno_idx = get_recno_index(table)

        with_table(table) do |fptr|
            # Skip header rec.

            fptr.readline

            # Take all the recnos you want to get, add the file positions

            # to them, and sort by file position, so that when we seek 

            # through the physical file we are going in ascending file

            # position order, which should be fastest.

            recnos.collect { |r| [recno_idx[r], r] }.sort.each do |r|
                fptr.seek(r[0])
                rec, line_length = line_to_rec(fptr.readline, encrypted)

                next if rec.empty?

                raise "Index Corrupt!" unless rec[0].to_i == r[1]
                rec << r[0] << line_length
                recs << rec
            end
            return recs
        end
    end

    #-----------------------------------------------------------------------

    # get_rec_by_recno

    #-----------------------------------------------------------------------

    def get_rec_by_recno(table, recno)
        encrypted = table.encrypted?
        recno_idx = get_recno_index(table)

        return nil unless recno_idx.has_key?(recno)

        with_table(table) do |fptr|
            fptr.seek(recno_idx[recno])
            rec, line_length = line_to_rec(fptr.readline, encrypted)

            raise "Recno Index Corrupt for table %s!" % table.name if \
             rec.empty?

            raise "Recno Index Corrupt for table %s!" % table.name unless \
             rec[0].to_i == recno

            rec << recno_idx[recno] << line_length
            return rec
        end
    end

    #-----------------------------------------------------------------------

    # line_to_rec

    #-----------------------------------------------------------------------

    def line_to_rec(line, encrypted)
        line.chomp!
        line_length = line.length
        line = unencrypt_str(line) if encrypted
        line.strip!

        # Convert line to rec and return rec and line length.

        return line.split('|', -1), line_length
    end

    #-----------------------------------------------------------------------

    # insert_record

    #-----------------------------------------------------------------------

    def insert_record(table, rec)
        with_write_locked_table(table) do |fptr|
            # Auto-increment the record number field.

            rec_no = incr_rec_no_ctr(table, fptr)

            # Insert the newly created record number value at the beginning

            # of the field values.

            rec[0] = rec_no

            fptr.seek(0, IO::SEEK_END)
            fpos = fptr.tell

            write_record(table, fptr, 'end', rec.join('|'))

            add_to_indexes(table, rec, fpos)

            # Return the record number of the newly created record.

            return rec_no
        end
    end

    #-----------------------------------------------------------------------

    # update_records

    #-----------------------------------------------------------------------

    def update_records(table, recs)
        with_write_locked_table(table) do |fptr|
            recs.each do |rec|
                line = rec[:rec].join('|')

                # This doesn't actually 'delete' the line, it just

                # makes it all spaces.  That way, if the updated

                # record is the same or less length than the old

                # record, we can write the record back into the

                # same spot.  If the updated record is greater than

                # the old record, we will leave the now spaced-out

                # line and write the updated record at the end of

                # the file.

                write_record(table, fptr, rec[:fpos],
                 ' ' * rec[:line_length])
                if line.length > rec[:line_length]
                    fptr.seek(0, IO::SEEK_END)
                    new_fpos = fptr.tell
                    write_record(table, fptr, 'end', line)
                    incr_del_ctr(table, fptr)

                    update_recno_index(table, rec[:rec].first, new_fpos)
                else
                    write_record(table, fptr, rec[:fpos], line)
                end
                update_to_indexes(table, rec[:rec])
            end
            # Return the number of records updated.

            return recs.size
        end
    end

    #-----------------------------------------------------------------------

    # delete_records

    #-----------------------------------------------------------------------

    def delete_records(table, recs)
        with_write_locked_table(table) do |fptr|
            recs.each do |rec|
                # Go to offset within the file where the record is and

                # replace it with all spaces.

                write_record(table, fptr, rec.fpos, ' ' * rec.line_length)
                incr_del_ctr(table, fptr)

                delete_from_indexes(table, rec, rec.fpos)
            end

            # Return the number of records deleted.

            return recs.size
        end
    end

    #-----------------------------------------------------------------------

    # change_column_type

    #-----------------------------------------------------------------------

    def change_column_type(table, col_name, col_type)
        col_index = table.field_names.index(col_name)
        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
            else
                header_rec = line.split('|')
            end
            
            temp_fields = header_rec[col_index+3].split(':')
            temp_fields[1] = col_type.to_s
            header_rec[col_index+3] = temp_fields.join(':')

            if line[0..0] == 'Z'
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                new_fptr.write(header_rec.join('|') + "\n")
            end

            begin
                while true
                    new_fptr.write(fptr.readline)
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)
        end
    end

    #-----------------------------------------------------------------------

    # rename_column

    #-----------------------------------------------------------------------

    def rename_column(table, old_col_name, new_col_name)
        col_index = table.field_names.index(old_col_name)
        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
            else
                header_rec = line.split('|')
            end
            
            temp_fields = header_rec[col_index+3].split(':')
            temp_fields[0] = new_col_name.to_s
            header_rec[col_index+3] = temp_fields.join(':')

            if line[0..0] == 'Z'
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                new_fptr.write(header_rec.join('|') + "\n")
            end

            begin
                while true
                    new_fptr.write(fptr.readline)
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)
        end
    end

    #-----------------------------------------------------------------------

    # add_column

    #-----------------------------------------------------------------------

    def add_column(table, field_def, after)
        # Find the index position of where to insert the column, either at

        # the end (-1) or after the field specified.

        if after.nil? or table.field_names.last == after
            insert_after = -1
        else
            insert_after = table.field_names.index(after)+1
        end

        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
            else
                header_rec = line.split('|')
            end

            if insert_after == -1
                header_rec.insert(insert_after, field_def)
            else
                # Need to account for recno ctr, delete ctr, record class.

                header_rec.insert(insert_after+3, field_def)
            end

            if line[0..0] == 'Z'
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                new_fptr.write(header_rec.join('|') + "\n")
            end

            begin
                while true
                    line = fptr.readline.chomp

                    if table.encrypted?
                        temp_line = unencrypt_str(line)
                    else
                        temp_line = line
                    end

                    rec = temp_line.split('|', -1)
                    rec.insert(insert_after, '')

                    if table.encrypted?
                        new_fptr.write(encrypt_str(rec.join('|')) + "\n")
                    else
                        new_fptr.write(rec.join('|') + "\n")
                    end
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)
        end
    end

    #-----------------------------------------------------------------------

    # drop_column

    #-----------------------------------------------------------------------

    def drop_column(table, col_name)
        col_index = table.field_names.index(col_name)
        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
                header_rec.delete_at(col_index+3)
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                header_rec = line.split('|')
                header_rec.delete_at(col_index+3)
                new_fptr.write(header_rec.join('|') + "\n")
            end

            begin
                while true
                    line = fptr.readline.chomp

                    if table.encrypted?
                        temp_line = unencrypt_str(line)
                    else
                        temp_line = line
                    end

                    rec = temp_line.split('|', -1)
                    rec.delete_at(col_index)

                    if table.encrypted?
                        new_fptr.write(encrypt_str(rec.join('|')) + "\n")
                    else
                        new_fptr.write(rec.join('|') + "\n")
                    end
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)
        end
    end

    #-----------------------------------------------------------------------

    # rename_table

    #-----------------------------------------------------------------------

    def rename_table(old_tablename, new_tablename)
        old_full_path = File.join(@db.path, old_tablename.to_s + @db.ext)
        new_full_path = File.join(@db.path, new_tablename.to_s + @db.ext)
        File.rename(old_full_path, new_full_path)    
    end
    
    #-----------------------------------------------------------------------

    # add_index

    #-----------------------------------------------------------------------

    def add_index(table, col_names, index_no)
        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
            else
                header_rec = line.split('|')
            end
            
            col_names.each do |c|
                header_rec[table.field_names.index(c)+3] += \
                 ':Index->%d' % index_no
            end

            if line[0..0] == 'Z'
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                new_fptr.write(header_rec.join('|') + "\n")
            end

            begin
                while true
                    new_fptr.write(fptr.readline)
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)
        end
    end

    #-----------------------------------------------------------------------

    # drop_index

    #-----------------------------------------------------------------------

    def drop_index(table, col_names)
        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
            else
                header_rec = line.split('|')
            end
            
            col_names.each do |c|
                temp_field_def = \
                 header_rec[table.field_names.index(c)+3].split(':')
                temp_field_def = temp_field_def.delete_if {|x|
                    x =~ /Index->/
                }    
                header_rec[table.field_names.index(c)+3] = \
                 temp_field_def.join(':')
            end

            if line[0..0] == 'Z'
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                new_fptr.write(header_rec.join('|') + "\n")
            end

            begin
                while true
                    new_fptr.write(fptr.readline)
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)
        end
    end

    #-----------------------------------------------------------------------

    # change_column_default_value

    #-----------------------------------------------------------------------

    def change_column_default_value(table, col_name, value)
        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
            else
                header_rec = line.split('|')
            end
            
            if header_rec[table.field_names.index(col_name)+3] =~ \
             /Default->/
                hr_chunks = \
                 header_rec[table.field_names.index(col_name)+3].split(':')

                if value.nil?
                    hr_chunks = hr_chunks.delete_if { |x| x =~ /Default->/ }
                    header_rec[table.field_names.index(col_name)+3] = \
                     hr_chunks.join(':')
                else
                    hr_chunks.collect! do |x|
                        if x =~ /Default->/
                            'Default->%s' % value
                        else
                            x
                        end
                    end
                    header_rec[table.field_names.index(col_name)+3] = \
                     hr_chunks.join(':')
                end              
            else
                if value.nil?
                else
                    header_rec[table.field_names.index(col_name)+3] += \
                     ':Default->%s' % value
                end    
            end

            if line[0..0] == 'Z'
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                new_fptr.write(header_rec.join('|') + "\n")
            end

            begin
                while true
                    new_fptr.write(fptr.readline)
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)
        end
    end

    #-----------------------------------------------------------------------

    # change_column_required

    #-----------------------------------------------------------------------

    def change_column_required(table, col_name, required)
        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
            else
                header_rec = line.split('|')
            end
            
            if header_rec[table.field_names.index(col_name)+3
             ] =~ /Required->/
                hr_chunks = \
                 header_rec[table.field_names.index(col_name)+3].split(':')
                if not required
                    hr_chunks = hr_chunks.delete_if {|x| x =~ /Required->/}
                    header_rec[table.field_names.index(col_name)+3] = \
                     hr_chunks.join(':')
                else
                    hr_chunks.collect! do |x|
                        if x =~ /Required->/
                            'Default->%s' % required
                        else
                            x
                        end
                    end
                    header_rec[table.field_names.index(col_name)+3] = \
                     hr_chunks.join(':')
                end    
            else
                if not required
                else
                    header_rec[table.field_names.index(col_name)+3] += \
                     ':Required->%s' % required
                end    
            end

            if line[0..0] == 'Z'
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                new_fptr.write(header_rec.join('|') + "\n")
            end

            begin
                while true
                    new_fptr.write(fptr.readline)
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)
        end
    end

    #-----------------------------------------------------------------------

    # pack_table

    #-----------------------------------------------------------------------

    def pack_table(table)
        with_write_lock(table.name) do
            fptr = open(table.filename, 'r')
            new_fptr = open(table.filename+'temp', 'w')

            line = fptr.readline.chomp
            # Reset the delete counter in the header rec to 0.

            if line[0..0] == 'Z'
                header_rec = unencrypt_str(line[1..-1]).split('|')
                header_rec[1] = '000000'
                new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
                 "\n")
            else
                header_rec = line.split('|')
                header_rec[1] = '000000'
                new_fptr.write(header_rec.join('|') + "\n")
            end

            lines_deleted = 0

            begin
                while true
                    line = fptr.readline

                    if table.encrypted?
                        temp_line = unencrypt_str(line)
                    else
                        temp_line = line
                    end

                    if temp_line.strip == ''
                        lines_deleted += 1
                    else
                        new_fptr.write(line)
                    end
                end
            # Here's how we break out of the loop...

            rescue EOFError
            end

            # Close the table and release the write lock.

            fptr.close
            new_fptr.close
            File.delete(table.filename)
            FileUtils.mv(table.filename+'temp', table.filename)

            # Return the number of deleted records that were removed.

            return lines_deleted
        end
    end

    #-----------------------------------------------------------------------

    # read_memo_file

    #-----------------------------------------------------------------------

    def read_memo_file(filepath)
        begin
            f = File.new(File.join(@db.memo_blob_path, filepath))
            return f.read
        ensure
            f.close
        end
    end

    #-----------------------------------------------------------------------

    # write_memo_file

    #-----------------------------------------------------------------------

    def write_memo_file(filepath, contents)
        begin
            f = File.new(File.join(@db.memo_blob_path, filepath), 'w')
            f.write(contents)
        ensure
            f.close
        end
    end

    #-----------------------------------------------------------------------

    # read_blob_file

    #-----------------------------------------------------------------------

    def read_blob_file(filepath)
        begin
            f = File.new(File.join(@db.memo_blob_path, filepath), 'rb')
            return f.read
        ensure
            f.close
        end
    end

    #-----------------------------------------------------------------------

    # write_blob_file

    #-----------------------------------------------------------------------

    def write_blob_file(filepath, contents)
        begin
            f = File.new(File.join(@db.memo_blob_path, filepath), 'wb')
            f.write(contents)
        ensure
            f.close
        end
    end
    

    #-----------------------------------------------------------------------

    # PRIVATE METHODS

    #-----------------------------------------------------------------------

    private

    #-----------------------------------------------------------------------

    # with_table

    #-----------------------------------------------------------------------

    def with_table(table, access='r')
        begin
            yield fptr = open(table.filename, access)
        ensure
            fptr.close
        end
    end

    #-----------------------------------------------------------------------

    # with_write_lock

    #-----------------------------------------------------------------------

    def with_write_lock(tablename)
        begin
            write_lock(tablename) if @db.server?
            yield
        ensure
            write_unlock(tablename) if @db.server?
        end
    end

    #-----------------------------------------------------------------------

    # with_write_locked_table

    #-----------------------------------------------------------------------

    def with_write_locked_table(table, access='r+')
        begin
            write_lock(table.name) if @db.server?
            yield fptr = open(table.filename, access)
        ensure
            fptr.close
            write_unlock(table.name) if @db.server?
        end
    end

    #-----------------------------------------------------------------------

    # write_lock

    #-----------------------------------------------------------------------

    def write_lock(tablename)
        # Unless an key already exists in the hash holding mutex records

        # for this table, create a write key for this table in the mutex

        # hash.  Then, place a lock on that mutex.

        @mutex_hash[tablename] = Mutex.new unless (
         @mutex_hash.has_key?(tablename))
        @mutex_hash[tablename].lock

        return true
    end

    #----------------------------------------------------------------------

    # write_unlock

    #----------------------------------------------------------------------

    def write_unlock(tablename)
        # Unlock the write mutex for this table.

        @mutex_hash[tablename].unlock

        return true
    end

    #----------------------------------------------------------------------

    # write_record

    #----------------------------------------------------------------------

    def write_record(table, fptr, pos, record)
        if table.encrypted?
            temp_rec = encrypt_str(record)
        else
            temp_rec = record
        end

        # If record is to be appended, go to end of table and write

        # record, adding newline character.

        if pos == 'end'
            fptr.seek(0, IO::SEEK_END)
            fptr.write(temp_rec + "\n")
        else
            # Otherwise, overwrite another record (that's why we don't

            # add the newline character).

            fptr.seek(pos)
            fptr.write(temp_rec)
        end
    end

    #----------------------------------------------------------------------

    # write_header_record

    #----------------------------------------------------------------------

    def write_header_record(table, fptr, record)
        fptr.seek(0)

        if table.encrypted?
            fptr.write('Z' + encrypt_str(record) + "\n")
        else
            fptr.write(record + "\n")
        end
    end

    #----------------------------------------------------------------------

    # get_header_record

    #----------------------------------------------------------------------

    def get_header_record(table, fptr)
        fptr.seek(0)

        line = fptr.readline.chomp

        if line[0..0] == 'Z'
            return [true, unencrypt_str(line[1..-1])]
        else
            return [false, line]
        end
    end

    #-----------------------------------------------------------------------

    # incr_rec_no_ctr

    #-----------------------------------------------------------------------

    def incr_rec_no_ctr(table, fptr)
        encrypted, header_line = get_header_record(table, fptr)
        last_rec_no, rest_of_line = header_line.split('|', 2)
        last_rec_no = last_rec_no.to_i + 1

        write_header_record(table, fptr, ['%06d' % last_rec_no,
         rest_of_line].join('|'))

        # Return the new recno.

        return last_rec_no
    end

    #-----------------------------------------------------------------------

    # incr_del_ctr

    #-----------------------------------------------------------------------

    def incr_del_ctr(table, fptr)
        encrypted, header_line = get_header_record(table, fptr)
        last_rec_no, del_ctr, rest_of_line = header_line.split('|', 3)
        del_ctr = del_ctr.to_i + 1

        write_header_record(table, fptr, [last_rec_no, '%06d' % del_ctr,
         rest_of_line].join('|'))

        return true
    end
end


#---------------------------------------------------------------------------

# KBTable

#---------------------------------------------------------------------------

class KBTable
    include DRb::DRbUndumped
    include KBTypeConversionsMixin

    # Make constructor private.  KBTable instances should only be created

    # from KirbyBase#get_table.

    private_class_method :new

    VALID_FIELD_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
                         :DateTime, :Memo, :Blob, :ResultSet, :YAML]
    VALID_DEFAULT_TYPES = [:String, :Integer, :Float, :Boolean, :Date,
                           :Time, :DateTime, :YAML]
    VALID_INDEX_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
                         :DateTime]

    attr_reader :filename, :name, :table_class, :db, :lookup_key, \
                           :last_rec_no, :del_ctr

    #-----------------------------------------------------------------------

    # KBTable.valid_field_type?

    #-----------------------------------------------------------------------

    #++

    # Return true if valid field type.

    #

    # *field_type*:: Symbol specifying field type.

    #

    def KBTable.valid_field_type?(field_type)
        VALID_FIELD_TYPES.include?(field_type)
    end

    #-----------------------------------------------------------------------

    # KBTable.valid_data_type?

    #-----------------------------------------------------------------------

    #++

    # Return true if data is correct type, false otherwise.

    #

    # *data_type*:: Symbol specifying data type.

    # *value*:: Value to convert to String.

    #

    def KBTable.valid_data_type?(data_type, value)
        case data_type
        when /:String|:Blob/
            return false unless value.respond_to?(:to_str)
        when :Memo
            return false unless value.is_a?(KBMemo) 
        when :Blob
            return false unless value.is_a?(KBBlob) 
        when :Boolean
            return false unless value.is_a?(TrueClass) or value.is_a?(
             FalseClass)
        when :Integer
            return false unless value.respond_to?(:to_int)
        when :Float
            return false unless value.respond_to?(:to_f)
        when :Time
            return false unless value.is_a?(Time)     
        when :Date
            return false unless value.is_a?(Date)
        when :DateTime
            return false unless value.is_a?(DateTime)
        when :YAML
            return false unless value.respond_to?(:to_yaml)
        end
    
        return true
    end

    #-----------------------------------------------------------------------

    # KBTable.valid_default_type?

    #-----------------------------------------------------------------------

    #++

    # Return true if valid default type.

    #

    # *field_type*:: Symbol specifying field type.

    #

    def KBTable.valid_default_type?(field_type)
        VALID_DEFAULT_TYPES.include?(field_type)
    end

    #-----------------------------------------------------------------------

    # KBTable.valid_index_type?

    #-----------------------------------------------------------------------

    #++

    # Return true if valid index type.

    #

    # *field_type*:: Symbol specifying field type.

    #

    def KBTable.valid_index_type?(field_type)
        VALID_INDEX_TYPES.include?(field_type)
    end

    #-----------------------------------------------------------------------

    # create_called_from_database_instance

    #-----------------------------------------------------------------------

    #++

    # Return a new instance of KBTable.  Should never be called directly by

    # your application.  Should only be called from KirbyBase#get_table.

    #

    def KBTable.create_called_from_database_instance(db, name, filename)
        return new(db, name, filename)
    end

    #-----------------------------------------------------------------------

    # initialize

    #-----------------------------------------------------------------------

    #++

    # This has been declared private so user's cannot create new instances

    # of KBTable from their application.  A user gets a handle to a KBTable

    # instance by calling KirbyBase#get_table for an existing table or

    # KirbyBase.create_table for a new table.

    #

    def initialize(db, name, filename)
        @db = db
        @name = name
        @filename = filename
        @encrypted = false
        @lookup_key = :recno
        @idx_timestamps = {}
        @idx_arrs = {}

        # Alias delete_all to clear method.

        alias delete_all clear

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end

    #-----------------------------------------------------------------------

    # encrypted?

    #-----------------------------------------------------------------------

    #++

    # Returns true if table is encrypted.

    #

    def encrypted?
        if @encrypted
            return true
        else
            return false
        end
    end

    #-----------------------------------------------------------------------

    # field_names

    #-----------------------------------------------------------------------

    #++

    # Return array containing table field names.

    #

    def field_names
        return @field_names
    end

    #-----------------------------------------------------------------------

    # field_types

    #-----------------------------------------------------------------------

    #++

    # Return array containing table field types.

    #

    def field_types
        return @field_types
    end

    #-----------------------------------------------------------------------

    # field_extras

    #-----------------------------------------------------------------------

    #++

    # Return array containing table field extras.

    #

    def field_extras
        return @field_extras
    end

    #-----------------------------------------------------------------------

    # field_indexes

    #-----------------------------------------------------------------------

    #++

    # Return array containing table field indexes.

    #

    def field_indexes
        return @field_indexes
    end

    #-----------------------------------------------------------------------

    # field_defaults

    #-----------------------------------------------------------------------

    #++

    # Return array containing table field defaults.

    #

    def field_defaults
        return @field_defaults
    end

    #-----------------------------------------------------------------------

    # field_requireds

    #-----------------------------------------------------------------------

    #++

    # Return array containing table field requireds.

    #

    def field_requireds
        return @field_requireds
    end

    #-----------------------------------------------------------------------

    # insert

    #-----------------------------------------------------------------------

    #++

    # Insert a new record into a table, return unique record number.

    #

    # *data*:: Array, Hash, Struct instance containing field values of

    #          new record.

    # *insert_proc*:: Proc instance containing insert code. This and the

    #                 data parameter are mutually exclusive.

    #

    def insert(*data, &insert_proc)
        raise 'Cannot specify both a hash/array/struct and a ' + \
         'proc for method #insert!' unless data.empty? or insert_proc.nil?

        raise 'Must specify either hash/array/struct or insert ' + \
         'proc for method #insert!' if data.empty? and insert_proc.nil?

        # Update the header variables.

        update_header_vars

        # Convert input, which could be a proc, an array, a hash, or a

        # Struct into a common format (i.e. hash).

        if data.empty?
            input_rec = convert_input_data(insert_proc)
        else
            input_rec = convert_input_data(data)
        end

        # Check the field values to make sure they are proper types.

        validate_input(input_rec)

        input_rec = Struct.new(*field_names).new(*field_names.zip(
         @field_defaults).collect do |fn, fd|
            if input_rec.has_key?(fn)
                input_rec[fn]
            else
                fd
            end
        end)

        check_required_fields(input_rec)

        check_against_input_for_specials(input_rec)

        new_recno = @db.engine.insert_record(self, @field_names.zip(
         @field_types).collect do |fn, ft|
            convert_to_encoded_string(ft, input_rec[fn])
        end)

        # If there are any associated memo/blob fields, save their values.

        input_rec.each { |r| r.write_to_file if r.is_a?(KBMemo) } if \
         @field_types.include?(:Memo)
        input_rec.each { |r| r.write_to_file if r.is_a?(KBBlob) } if \
         @field_types.include?(:Blob)
                        
        return new_recno
    end

    #-----------------------------------------------------------------------

    # update_all

    #-----------------------------------------------------------------------

    #++

    # Return array of records (Structs) to be updated, in this case all

    # records.

    #

    # *updates*:: Hash or Struct containing updates.

    #

    def update_all(*updates, &update_proc)
        raise 'Cannot specify both a hash/array/struct and a ' + \
         'proc for method #update_all!' unless updates.empty? or \
         update_proc.nil?

        raise 'Must specify either hash/array/struct or update ' + \
         'proc for method #update_all!' if updates.empty? and \
         update_proc.nil?

        # Depending on whether the user supplied an array/hash/struct or a

        # block as update criteria, we are going to call updates in one of

        # two ways.

        if updates.empty?
            update { true }.set &update_proc
        else
            update(*updates) { true }
        end
    end

    #-----------------------------------------------------------------------

    # update

    #-----------------------------------------------------------------------

    #++

    # Return array of records (Structs) to be updated based on select cond.

    #

    # *updates*:: Hash or Struct containing updates.

    # *select_cond*:: Proc containing code to select records to update.

    #

    def update(*updates, &select_cond)
        raise ArgumentError, "Must specify select condition code " + \
         "block.  To update all records, use #update_all instead." if \
         select_cond.nil?

        # Update the header variables.

        update_header_vars

        # Get all records that match the selection criteria and

        # return them in an array.

        result_set = get_matches(:update, @field_names, select_cond)

        # If updates is empty, this means that the user must have specified

        # the updates in KBResultSet#set, i.e.

        # tbl.update {|r| r.recno == 1}.set(:name => 'Bob')

        return result_set if updates.empty?

        # Call KBTable#set and pass it the records to be updated and the

        # updated criteria.

        set(result_set, updates)
    end

    #-----------------------------------------------------------------------

    # []=

    #-----------------------------------------------------------------------

    #++

    # Update record whose recno field equals index.

    #

    # *index*:: Integer specifying recno you wish to select.

    # *updates*:: Hash, Struct, or Array containing updates.

    #

    def []=(index, updates)
        return update(updates) { |r| r.recno == index }
    end

    #-----------------------------------------------------------------------

    # set

    #-----------------------------------------------------------------------

    #++

    # Set fields of records to updated values.  Returns number of records

    # updated.

    #

    # *recs*:: Array of records (Structs) that will be updated.

    # *data*:: Hash, Struct, Proc containing updates.

    #

    def set(recs, data)
        # If updates are not in the form of a Proc, convert updates, which

        # could be an array, a hash, or a Struct into a common format (i.e.

        # hash).

        update_rec = convert_input_data(data) unless data.is_a?(Proc)

        updated_recs = []

        # For each one of the recs that matched the update query, apply the

        # updates to it and write it back to the database table.

        recs.each do |rec|
            temp_rec = rec.dup

            if data.is_a?(Proc)
                begin
                    data.call(temp_rec)
                rescue NoMethodError
                    raise 'Invalid field name in code block: %s' % $!
                end
             else
                @field_names.each { |fn| temp_rec[fn] = update_rec.fetch(fn,
                 temp_rec.send(fn)) }
            end

            # Is the user trying to change something they shouldn't?

            raise 'Cannot update recno field!' unless \
             rec.recno == temp_rec.recno
            raise 'Cannot update internal fpos field!' unless \
             rec.fpos == temp_rec.fpos
            raise 'Cannot update internal line_length field!' unless \
             rec.line_length == temp_rec.line_length

            # Are the data types of the updates correct?

            validate_input(temp_rec)

            check_required_fields(temp_rec)

            check_against_input_for_specials(temp_rec)

            # Apply updates to the record and add it to an array holding

            # updated records.  We need the fpos and line_length because

            # the engine will use them to determine where to write the

            # update and whether the updated record will fit in the old

            # record's spot.

            updated_recs << { :rec => @field_names.zip(@field_types
             ).collect { |fn, ft| convert_to_encoded_string(ft,
             temp_rec.send(fn)) }, :fpos => rec.fpos,
             :line_length => rec.line_length }
 

            # Update any associated blob/memo fields.

            temp_rec.each { |r| r.write_to_file if r.is_a?(KBMemo) } if \
             @field_types.include?(:Memo)
            temp_rec.each { |r| r.write_to_file if r.is_a?(KBBlob) } if \
             @field_types.include?(:Blob)
        end

        # Take all of the update records and write them back out to the

        # table's file.

        @db.engine.update_records(self, updated_recs)

        # Return the number of records updated.

        return recs.size
    end

    #-----------------------------------------------------------------------

    # delete

    #-----------------------------------------------------------------------

    #++

    # Delete records from table and return # deleted.

    #

    # *select_cond*:: Proc containing code to select records.

    #

    def delete(&select_cond)
        raise ArgumentError, 'Must specify select condition code ' + \
         'block.  To delete all records, use #clear instead.' if \
         select_cond.nil?

        # Get all records that match the selection criteria and

        # return them in an array.

        result_set = get_matches(:delete, [:recno], select_cond)

        @db.engine.delete_records(self, result_set)

        # Return the number of records deleted.

        return result_set.size
    end

    #-----------------------------------------------------------------------

    # clear

    #-----------------------------------------------------------------------

    #++

    # Delete all records from table. You can also use #delete_all.

    #

    # *reset_recno_ctr*:: true/false specifying whether recno counter should

    #                     be reset to 0.

    #

    def clear(reset_recno_ctr=true)
        recs_deleted = delete { true }
        pack

        @db.engine.reset_recno_ctr(self) if reset_recno_ctr
        update_header_vars
        return recs_deleted
    end

    #-----------------------------------------------------------------------

    # []

    #-----------------------------------------------------------------------

    #++

    # Return the record(s) whose recno field is included in index.

    #

    # *index*:: Array of Integer(s) specifying recno(s) you wish to select.

    #

    def [](*index)
        return nil if index[0].nil?

        return get_match_by_recno(:select, @field_names, index[0]) if \
         index.size == 1

        recs = select_by_recno_index(*@field_names) { |r|
            index.include?(r.recno)
        }

        return recs
    end

    #-----------------------------------------------------------------------

    # select

    #-----------------------------------------------------------------------

    #++

    # Return array of records (Structs) matching select conditions.

    #

    # *filter*:: List of field names (Symbols) to include in result set.

    # *select_cond*:: Proc containing select code.

    #

    def select(*filter, &select_cond)
        # Declare these variables before the code block so they don't go

        # after the code block is done.

        result_set = []

        # Validate that all names in filter are valid field names.

        validate_filter(filter)

        filter = @field_names if filter.empty?

        # Get all records that match the selection criteria and

        # return them in an array of Struct instances.

        return get_matches(:select, filter, select_cond)
    end

    #-----------------------------------------------------------------------

    # select_by_recno_index

    #-----------------------------------------------------------------------

    #++

    # Return array of records (Structs) matching select conditions.  Select

    # condition block should not contain references to any table column

    # except :recno.  If you need to select by other table columns than just

    # :recno, use #select instead.

    #

    # *filter*:: List of field names (Symbols) to include in result set.

    # *select_cond*:: Proc containing select code.

    #

    def select_by_recno_index(*filter, &select_cond)
        # Declare these variables before the code block so they don't go

        # after the code block is done.

        result_set = []

        # Validate that all names in filter are valid field names.

        validate_filter(filter)

        filter = @field_names if filter.empty?

        # Get all records that match the selection criteria and

        # return them in an array of Struct instances.

        return get_matches_by_recno_index(:select, filter, select_cond)
    end

    #-----------------------------------------------------------------------

    # pack

    #-----------------------------------------------------------------------

    #++

    # Remove blank records from table, return total removed.

    #

    def pack
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        lines_deleted = @db.engine.pack_table(self)

        update_header_vars

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)
        create_indexes
        create_table_class unless @db.server?

        return lines_deleted
    end

    #-----------------------------------------------------------------------

    # rename_column

    #-----------------------------------------------------------------------

    #++

    # Rename a column.

    #

    # Make sure you are executing this method while in single-user mode

    # (i.e. not running in client/server mode).

    #

    # *old_col_name*:: Symbol of old column name.

    # *new_col_name*:: Symbol of new column name.

    #

    def rename_column(old_col_name, new_col_name)
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        raise "Cannot rename recno column!" if old_col_name == :recno
        raise "Cannot give column name of recno!" if new_col_name == :recno

       raise 'Invalid column name to rename: ' % old_col_name unless \
         @field_names.include?(old_col_name)
        
       raise 'New column name already exists: ' % new_col_name if \
         @field_names.include?(new_col_name)

        @db.engine.rename_column(self, old_col_name, new_col_name)

        # Need to reinitialize the table instance and associated indexes.

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end

    #-----------------------------------------------------------------------

    # change_column_type

    #-----------------------------------------------------------------------

    #++

    # Change a column's type.

    #

    # Make sure you are executing this method while in single-user mode

    # (i.e. not running in client/server mode).

    #

    # *col_name*:: Symbol of column name.

    # *col_type*:: Symbol of new column type.

    #

    def change_column_type(col_name, col_type)
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        raise "Cannot change type for recno column!" if col_name == :recno
        raise 'Invalid column name: ' % col_name unless \
         @field_names.include?(col_name)
        
        raise 'Invalid field type: %s' % col_type unless \
         KBTable.valid_field_type?(col_type)

        @db.engine.change_column_type(self, col_name, col_type)

        # Need to reinitialize the table instance and associated indexes.

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end

    #-----------------------------------------------------------------------

    # add_column

    #-----------------------------------------------------------------------

    #++

    # Add a column to table.

    #

    # Make sure you are executing this method while in single-user mode

    # (i.e. not running in client/server mode).

    #

    # *col_name*:: Symbol of column name to add.

    # *col_type*:: Symbol (or Hash if includes field extras) of column type

    #              to add.

    # *after*:: Symbol of column name that you want to add this column

    #           after.

    #

    def add_column(col_name, col_type, after=nil)
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        raise "Invalid column name in 'after': #{after}" unless after.nil? \
         or @field_names.include?(after)

        raise "Invalid column name in 'after': #{after}" if after == :recno

        raise "Column name cannot be recno!" if col_name == :recno
        
        raise "Column name already exists!" if @field_names.include?(
         col_name)

        # Does this new column have field extras (i.e. Index, Lookup, etc.)

        if col_type.is_a?(Hash)
            temp_type = col_type[:DataType]
        else
            temp_type = col_type
        end

        raise 'Invalid field type: %s' % temp_type unless \
         KBTable.valid_field_type?(temp_type)

        field_def = @db.build_header_field_string(col_name, col_type)

        @db.engine.add_column(self, field_def, after)

        # Need to reinitialize the table instance and associated indexes.

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end

    #-----------------------------------------------------------------------

    # drop_column

    #-----------------------------------------------------------------------

    #++

    # Drop a column from table.

    #

    # Make sure you are executing this method while in single-user mode

    # (i.e. not running in client/server mode). 

    #

    # *col_name*:: Symbol of column name to add.

    #

    def drop_column(col_name)
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        raise 'Invalid column name: ' % col_name unless \
         @field_names.include?(col_name)

        raise "Cannot drop :recno column!" if col_name == :recno

        @db.engine.drop_column(self, col_name)

        # Need to reinitialize the table instance and associated indexes.

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end

    #-----------------------------------------------------------------------

    # add_index

    #-----------------------------------------------------------------------

    #++

    # Add an index to a column.

    #

    # Make sure you are executing this method while in single-user mode

    # (i.e. not running in client/server mode).

    #

    # *col_names*:: Array containing column name(s) of new index.

    #

    def add_index(*col_names)
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        col_names.each do |c|
            raise "Invalid column name: #{c}" unless \
             @field_names.include?(c)
            
            raise "recno column cannot be indexed!" if c == :recno

            raise "Column already indexed: #{c}" unless \
             @field_indexes[@field_names.index(c)].nil?
        end
        
        last_index_no_used = 0
        @field_indexes.each do |i|
            next if i.nil?
            index_no = i[-1..-1].to_i
            last_index_no_used = index_no if index_no > last_index_no_used
        end
        
        @db.engine.add_index(self, col_names, last_index_no_used+1)

        # Need to reinitialize the table instance and associated indexes.

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end

    #-----------------------------------------------------------------------

    # drop_index

    #-----------------------------------------------------------------------

    #++

    # Drop an index on a column(s).

    #

    # Make sure you are executing this method while in single-user mode

    # (i.e. not running in client/server mode).

    #

    # *col_names*:: Array containing column name(s) of new index.

    #

    def drop_index(*col_names)
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        col_names.each do |c|
            raise "Invalid column name: #{c}" unless \
             @field_names.include?(c)
            
            raise "recno column index cannot be dropped!" if c == :recno

            raise "Column not indexed: #{c}" if \
             @field_indexes[@field_names.index(c)].nil?
        end
        
        @db.engine.drop_index(self, col_names)

        # Need to reinitialize the table instance and associated indexes.

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end

    #-----------------------------------------------------------------------

    # change_column_default_value

    #-----------------------------------------------------------------------

    #++

    # Change a column's default value.

    #

    # Make sure you are executing this method while in single-user mode

    # (i.e. not running in client/server mode).

    #

    # *col_name*:: Symbol of column name.

    # *value*:: New default value for column.

    #

    def change_column_default_value(col_name, value)
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        raise ":recno cannot have a default value!" if col_name == :recno

        raise 'Invalid column name: ' % col_name unless \
         @field_names.include?(col_name)

        raise 'Cannot set default value for this type: ' + \
         '%s' % @field_types.index(col_name) unless \
         KBTable.valid_default_type?(
          @field_types[@field_names.index(col_name)])
        
        if value.nil?
            @db.engine.change_column_default_value(self, col_name, nil)
        else
            @db.engine.change_column_default_value(self, col_name,
             convert_to_encoded_string(
              @field_types[@field_names.index(col_name)], value))
        end
            
        # Need to reinitialize the table instance and associated indexes.

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end
    
    #-----------------------------------------------------------------------

    # change_column_required

    #-----------------------------------------------------------------------

    #++

    # Change whether a column is required.

    #

    # Make sure you are executing this method while in single-user mode

    # (i.e. not running in client/server mode).

    #

    # *col_name*:: Symbol of column name.

    # *required*:: true or false.

    #

    def change_column_required(col_name, required)
        raise "Do not execute this method in client/server mode!" if \
         @db.client?

        raise ":recno is always required!" if col_name == :recno

        raise 'Invalid column name: ' % col_name unless \
         @field_names.include?(col_name)
        
        raise 'Required must be either true or false!' unless \
         [true, false].include?(required)
        
        @db.engine.change_column_required(self, col_name, required)
            
        # Need to reinitialize the table instance and associated indexes.

        @db.engine.remove_recno_index(@name)
        @db.engine.remove_indexes(@name)

        update_header_vars
        create_indexes
        create_table_class unless @db.server?
    end
    
    #-----------------------------------------------------------------------

    # total_recs

    #-----------------------------------------------------------------------

    #++

    # Return total number of undeleted (blank) records in table.

    #

    def total_recs
        return @db.engine.get_total_recs(self)
    end

    #-----------------------------------------------------------------------

    # import_csv

    #-----------------------------------------------------------------------

    #++

    # Import csv file into table.

    #

    # *csv_filename*:: filename of csv file to import.

    #

    def import_csv(csv_filename)
        records_inserted = 0
        tbl_rec = @table_class.new(self)

        # read with FasterCSV if loaded, or the standard CSV otherwise

        (defined?(FasterCSV) ? FasterCSV : CSV).foreach(csv_filename
         ) do |row|
            tbl_rec.populate([nil] + row)
            insert(tbl_rec)
            records_inserted += 1
        end
        return records_inserted
    end

    #-----------------------------------------------------------------------

    # PRIVATE METHODS

    #-----------------------------------------------------------------------

    private

    #-----------------------------------------------------------------------

    # create_indexes

    #-----------------------------------------------------------------------

    def create_indexes
        # First remove any existing select_by_index methods.  This is in

        # case we are dropping an index or a column.  We want to make sure

        # an select_by_index method doesn't hang around if it's index or

        # column has been dropped.

        methods.each do |m|
            next if m == 'select_by_recno_index'
            
            if m =~ /select_by_.*_index/
                class << self; self end.send(:remove_method, m.to_sym)
            end    
        end

        # Create the recno index.  A recno index always gets created even if

        # there are no user-defined indexes for the table.

        @db.engine.init_recno_index(self) 

        # There can be up to 5 different indexes on a table.  Any of these

        # indexes can be single or compound.

        ['Index->1', 'Index->2', 'Index->3', 'Index->4',
         'Index->5'].each do |idx|
            index_col_names = []
            @field_indexes.each_with_index do |fi,i|
                next if fi.nil?
                index_col_names << @field_names[i] if fi.include?(idx)
            end

            # If no fields were indexed on this number (1..5), go to the

            # next index number.

            next if index_col_names.empty?

            # Create this index on the engine.

            @db.engine.init_index(self, index_col_names) 

            # For each index found, add an instance method for it so that

            # it can be used for #selects.

            select_meth_str = "def select_by_\#{index_col_names.join('_')}_index(*filter,\n&select_cond)\nresult_set = []\nvalidate_filter(filter)\nfilter = @field_names if filter.empty?\nreturn get_matches_by_index(:select,\n[:\#{index_col_names.join(',:')}], filter, select_cond)\nend\n"

            instance_eval(select_meth_str) unless @db.server?
           
            @idx_timestamps[index_col_names.join('_')] = nil
            @idx_arrs[index_col_names.join('_')] = nil
        end
    end

    #-----------------------------------------------------------------------

    # create_table_class

    #-----------------------------------------------------------------------

    def create_table_class
        #This is the class that will be used in #select condition blocks.

        @table_class = Class.new(KBTableRec)

        get_meth_str = ''
        get_meth_upd_res_str = ''
        set_meth_str = ''

        @field_names.zip(@field_types, @field_extras) do |x|
            field_name, field_type, field_extra = x

            @lookup_key = field_name if field_extra.has_key?('Key')

            # These are the default get/set methods for the table column.

            get_meth_str = "def \#{field_name}\nreturn @\#{field_name}\nend\n"
            get_meth_upd_res_str = "def \#{field_name}_upd_res\nreturn @\#{field_name}\nend\n"
            set_meth_str = "def \#{field_name}=(s)\n@\#{field_name} = convert_to_native_type(:\#{field_type}, s)\nend\n"

            # If this is a Lookup field, modify the get_method.

            if field_extra.has_key?('Lookup')
                lookup_table, key_field = field_extra['Lookup'].split('.')

                # If joining to recno field of lookup table use the

                # KBTable[] method to get the record from the lookup table.

                if key_field == 'recno'
                    get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{lookup_table})\nreturn table[@\#{field_name}]\nend\n"
                else
                    begin
                        unless @db.get_table(lookup_table.to_sym
                         ).respond_to?('select_by_%s_index' % key_field)
                            raise RuntimeError
                        end
                        
                        get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{lookup_table})\nreturn table.select_by_\#{key_field}_index { |r|\nr.\#{key_field} == @\#{field_name} }[0]\nend\n"
                    rescue RuntimeError
                        get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{lookup_table})\nreturn table.select { |r|\nr.\#{key_field} == @\#{field_name} }[0]\nend\n"
                    end
                end
            end

            # If this is a Link_many field, modify the get/set methods.

            if field_extra.has_key?('Link_many')
                lookup_field, rest = field_extra['Link_many'].split('=')
                link_table, link_field = rest.split('.')

                begin
                    unless @db.get_table(link_table.to_sym).respond_to?(
                     'select_by_%s_index' % link_field)
                        raise RuntimeError
                    end
                    
                    get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{link_table})\nreturn table.select_by_\#{link_field}_index { |r|\nr.send(:\#{link_field}) == @\#{lookup_field} }\nend\n"
                rescue RuntimeError
                    get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{link_table})\nreturn table.select { |r|\nr.send(:\#{link_field}) == @\#{lookup_field} }\nend\n"
                end

                get_meth_upd_res_str = "def \#{field_name}_upd_res\nreturn kb_nil\nend\n"
                set_meth_str = "def \#{field_name}=(s)\n@\#{field_name} = kb_nil\nend\n"
            end

            # If this is a Calculated field, modify the get/set methods.

            if field_extra.has_key?('Calculated')
                calculation = field_extra['Calculated']

                get_meth_str = "def \#{field_name}()\nreturn \#{calculation}\nend\n"
                get_meth_upd_res_str = "def \#{field_name}_upd_res()\nreturn kb_nil\nend\n"
                set_meth_str = "def \#{field_name}=(s)\n@\#{field_name} = kb_nil\nend\n"
            end

            @table_class.class_eval(get_meth_str)
            @table_class.class_eval(get_meth_upd_res_str)
            @table_class.class_eval(set_meth_str)
        end
    end

    #-----------------------------------------------------------------------

    # validate_filter

    #-----------------------------------------------------------------------

    #++

    # Check that filter contains valid field names.

    #

    def validate_filter(filter)
        # Each field in the filter array must be a valid fieldname in the

        # table.

        filter.each { |f|
            raise 'Invalid field name: %s in filter!' % f unless \
             @field_names.include?(f)
        }
    end

    #-----------------------------------------------------------------------

    # convert_input_data

    #-----------------------------------------------------------------------

    #++

    # Convert data passed to #input, #update, or #set to a common format.

    #

    def convert_input_data(values)
        temp_hash = {}

        # This only applies to Procs in #insert, Procs in #update are

        # handled in #set.

        if values.is_a?(Proc)
            tbl_rec = Struct.new(*@field_names[1..-1]).new
            begin
                values.call(tbl_rec)
            rescue NoMethodError
                raise 'Invalid field name in code block: %s' % $!
            end

            @field_names[1..-1].each do |f|
                temp_hash[f] = tbl_rec[f] unless tbl_rec[f].nil?
            end

        # Is input data an instance of custom record class, Struct, or

        # KBTableRec?

        elsif values.first.is_a?(Object.full_const_get(@record_class)) or \
         values.first.is_a?(Struct) or values.first.class == @table_class
            @field_names[1..-1].each do |f|
                temp_hash[f] = values.first.send(f) if \
                 values.first.respond_to?(f)
            end

        # Is input data a hash?

        elsif values.first.is_a?(Hash)
            temp_hash = values.first.dup

        # Is input data an array?

        elsif values.is_a?(Array)
            raise ArgumentError, 'Must specify all fields in input array!' \
             unless values.size == @field_names[1..-1].size

            @field_names[1..-1].each do |f|
                temp_hash[f] = values[@field_names.index(f)-1]
            end
        else
            raise(ArgumentError, 'Invalid type for values container!')
        end

        return temp_hash
    end

    #-----------------------------------------------------------------------

    # check_required_fields

    #-----------------------------------------------------------------------

    #++

    # Check that all required fields have values.

    #

    def check_required_fields(data)
        @field_names[1..-1].each do |f|
            raise(ArgumentError,
             'A value for this field is required: %s' % f) if \
             @field_requireds[@field_names.index(f)] and data[f].nil? 
        end
    end

    #-----------------------------------------------------------------------

    # check_against_input_for_specials

    #-----------------------------------------------------------------------

    #++

    # Check that no special field types (i.e. calculated or link_many

    # fields)

    # have been given values.

    #

    def check_against_input_for_specials(data)
        @field_names[1..-1].each do |f|
            raise(ArgumentError,
             'You cannot input a value for this field: %s' % f) if \
             @field_extras[@field_names.index(f)].has_key?('Calculated') \
             or @field_extras[@field_names.index(f)].has_key?('Link_many') \
              and not data[f].nil? 
        end
    end

    #-----------------------------------------------------------------------

    # validate_input

    #-----------------------------------------------------------------------

    #++

    # Check input data to ensure proper data types.

    #

    def validate_input(data)
        @field_names[1..-1].each do |f|
            next if data[f].nil?

            raise 'Invalid data %s for column %s' % [data[f], f] unless \
             KBTable.valid_data_type?(@field_types[@field_names.index(f)],
             data[f])
        end
    end

    #-----------------------------------------------------------------------

    # update_header_vars

    #-----------------------------------------------------------------------

    #++

    # Read header record and update instance variables.

    #

    def update_header_vars
        @encrypted, @last_rec_no, @del_ctr, @record_class, @col_names, \
         @col_types, @col_indexes, @col_defaults, @col_requireds, \
         @col_extras = @db.engine.get_header_vars(self)

        # These are deprecated.

        @field_names = @col_names
        @field_types = @col_types
        @field_indexes = @col_indexes
        @field_defaults = @col_defaults
        @field_requireds = @col_requireds
        @field_extras = @col_extras
    end

    #-----------------------------------------------------------------------

    # get_result_struct

    #-----------------------------------------------------------------------

    #++

    # Return Struct object that will hold result record.

    #

    def get_result_struct(query_type, filter)
        case query_type
        when :select
            return Struct.new(*filter) if @record_class == 'Struct'
        when :update
            return Struct.new(*(filter + [:fpos, :line_length]))
        when :delete
            return Struct.new(:recno, :fpos, :line_length)
        end
        return nil
    end

    #-----------------------------------------------------------------------

    # create_result_rec

    #-----------------------------------------------------------------------

    #++

    # Return Struct/custom class populated with table row data.

    #

    def create_result_rec(query_type, filter, result_struct, tbl_rec, rec)
        # If this isn't a select query or if it is a select query, but

        # the table record class is simply a Struct, then we will use

        # a Struct for the result record type.

        if query_type != :select
            result_rec = result_struct.new(*filter.collect { |f|
             tbl_rec.send("#{f}_upd_res".to_sym) })
        elsif @record_class == 'Struct'
            result_rec = result_struct.new(*filter.collect do |f|
                if tbl_rec.send(f).kb_nil?
                    nil
                else
                    tbl_rec.send(f)
                end
            end)
        else
            if Object.full_const_get(@record_class).respond_to?(:kb_create)
                result_rec = Object.full_const_get(@record_class
                 ).kb_create(*@field_names.collect do |f|
                    # Just a warning here:  If you specify a filter on

                    # a select, you are only going to get those fields

                    # you specified in the result set, EVEN IF

                    # record_class is a custom class instead of Struct.

                    if filter.include?(f)
                        if tbl_rec.send(f).kb_nil?
                            nil
                        else
                            tbl_rec.send(f)
                        end
                    else
                        nil
                    end
                end)
            elsif Object.full_const_get(@record_class).respond_to?(
             :kb_defaults)
                result_rec = Object.full_const_get(@record_class).new(
                 *@field_names.collect do |f|
                    if tbl_rec.send(f).kb_nil?
                        nil
                    else
                        tbl_rec.send(f) || Object.full_const_get(
                         @record_class).kb_defaults[@field_names.index(f)]
                    end
                end)
            else
                result_rec = Object.full_const_get(@record_class).allocate
                @field_names.each do |fn|
                    if tbl_rec.send(fn).kb_nil?
                        result_rec.send("#{fn}=", nil)
                    else
                        result_rec.send("#{fn}=", tbl_rec.send(fn))
                    end
                end
            end
        end

        unless query_type == :select
            result_rec.fpos = rec[-2]
            result_rec.line_length = rec[-1]
        end
        return result_rec
    end

    #-----------------------------------------------------------------------

    # get_matches

    #-----------------------------------------------------------------------

    #++

    # Return records from table that match select condition.

    #

    def get_matches(query_type, filter, select_cond)
        result_struct = get_result_struct(query_type, filter)
        match_array = KBResultSet.new(self, filter, filter.collect { |f|
         @field_types[@field_names.index(f)] })

        tbl_rec = @table_class.new(self)

        # Loop through table.

        @db.engine.get_recs(self).each do |rec|
            tbl_rec.populate(rec)

            next if select_cond and not select_cond.call(tbl_rec)

            match_array << create_result_rec(query_type, filter,
             result_struct, tbl_rec, rec)
        end
        return match_array
    end

    #-----------------------------------------------------------------------

    # get_matches_by_index

    #-----------------------------------------------------------------------

    #++

    # Return records from table that match select condition using one of

    # the table's indexes instead of searching the whole file.

    #

    def get_matches_by_index(query_type, index_fields, filter, select_cond)
        good_matches = []

        idx_struct = Struct.new(*(index_fields + [:recno]))

        begin
            if @db.client?
                # If client, check to see if the copy of the index we have

                # is up-to-date.  If it is not up-to-date, grab a new copy

                # of the index array from the engine.

                unless @idx_timestamps[index_fields.join('_')] == \
                 @db.engine.get_index_timestamp(self, index_fields.join(
                 '_'))
                    @idx_timestamps[index_fields.join('_')] = \
                     @db.engine.get_index_timestamp(self, index_fields.join(
                     '_'))
                     
                    @idx_arrs[index_fields.join('_')] = \
                     @db.engine.get_index(self, index_fields.join('_'))
                end
            else
                # If running single-user, grab the index array from the 

                # engine.

                @idx_arrs[index_fields.join('_')] = \
                 @db.engine.get_index(self, index_fields.join('_'))
            end

            @idx_arrs[index_fields.join('_')].each do |rec|
                good_matches << rec[-1] if select_cond.call(
                 idx_struct.new(*rec))
            end
        rescue NoMethodError
            raise 'Field name in select block not part of index!'
        end

        return get_matches_by_recno(query_type, filter, good_matches)
    end

    #-----------------------------------------------------------------------

    # get_matches_by_recno_index

    #-----------------------------------------------------------------------

    #++

    # Return records from table that match select condition using the

    # table's recno index instead of searching the whole file.

    #

    def get_matches_by_recno_index(query_type, filter, select_cond)
        good_matches = []
        idx_struct = Struct.new(:recno)

        begin
            @db.engine.get_recno_index(self).each_key do |key|
                good_matches << key if select_cond.call(idx_struct.new(key))
            end
        rescue NoMethodError
            raise "You can only use recno field in select block!"
        end

        return nil if good_matches.empty?
        return get_matches_by_recno(query_type, filter, good_matches)
    end

    #-----------------------------------------------------------------------

    # get_match_by_recno

    #-----------------------------------------------------------------------

    #++

    # Return record from table that matches supplied recno.

    #

    def get_match_by_recno(query_type, filter, recno)
        result_struct = get_result_struct(query_type, filter)
        match_array = KBResultSet.new(self, filter, filter.collect { |f|
         @field_types[@field_names.index(f)] })

        tbl_rec = @table_class.new(self)

        rec = @db.engine.get_rec_by_recno(self, recno)
        return nil if rec.nil?
        tbl_rec.populate(rec)

        return create_result_rec(query_type, filter, result_struct,
         tbl_rec, rec)
    end

    #-----------------------------------------------------------------------

    # get_matches_by_recno

    #-----------------------------------------------------------------------

    #++

    # Return records from table that match select condition.

    #

    def get_matches_by_recno(query_type, filter, recnos)
        result_struct = get_result_struct(query_type, filter)
        match_array = KBResultSet.new(self, filter, filter.collect { |f|
         @field_types[@field_names.index(f)] })

        tbl_rec = @table_class.new(self)

        @db.engine.get_recs_by_recno(self, recnos).each do |rec|
            next if rec.nil?
            tbl_rec.populate(rec)

            match_array << create_result_rec(query_type, filter,
             result_struct, tbl_rec, rec)
        end
        return match_array
    end
end


#---------------------------------------------------------------------------

# KBMemo

#---------------------------------------------------------------------------

class KBMemo
    attr_accessor :filepath, :contents

    #-----------------------------------------------------------------------

    # initialize

    #-----------------------------------------------------------------------

    def initialize(db, filepath, contents='')
        @db = db
        @filepath = filepath
        @contents = contents
    end
    
    #-----------------------------------------------------------------------

    # read_from_file

    #-----------------------------------------------------------------------

    def read_from_file
        @contents = @db.engine.read_memo_file(@filepath)
    end
    
    #-----------------------------------------------------------------------

    # write_to_file

    #-----------------------------------------------------------------------

    def write_to_file
        @db.engine.write_memo_file(@filepath, @contents)
    end
end


#---------------------------------------------------------------------------

# KBBlob

#---------------------------------------------------------------------------

class KBBlob
    attr_accessor :filepath, :contents

    #-----------------------------------------------------------------------

    # initialize

    #-----------------------------------------------------------------------

    def initialize(db, filepath, contents='')
        @db = db
        @filepath = filepath
        @contents = contents
    end
    
    #-----------------------------------------------------------------------

    # read_from_file

    #-----------------------------------------------------------------------

    def read_from_file
        @contents = @db.engine.read_blob_file(@filepath)
    end

    #-----------------------------------------------------------------------

    # write_to_file

    #-----------------------------------------------------------------------

    def write_to_file
        @db.engine.write_blob_file(@filepath, @contents)
    end
end


#---------------------------------------------------------------------------

# KBIndex

#---------------------------------------------------------------------------

class KBIndex
    include KBTypeConversionsMixin
    include KBEncryptionMixin

    #-----------------------------------------------------------------------

    # initialize

    #-----------------------------------------------------------------------

    def initialize(table, index_fields)
        @last_update = Time.new
        @idx_arr = []
        @table = table
        @index_fields = index_fields
        @col_poss = index_fields.collect {|i| table.field_names.index(i) }
        @col_names = index_fields
        @col_types = index_fields.collect {|i|
         table.field_types[table.field_names.index(i)]}
    end

    #-----------------------------------------------------------------------

    # get_idx

    #-----------------------------------------------------------------------

    def get_idx
        return @idx_arr
    end

    #-----------------------------------------------------------------------

    # get_timestamp

    #-----------------------------------------------------------------------

    def get_timestamp
        return @last_update
    end

    #-----------------------------------------------------------------------

    # rebuild

    #-----------------------------------------------------------------------

    def rebuild(fptr)
        @idx_arr.clear

        encrypted = @table.encrypted?

        # Skip header rec.

        fptr.readline

        begin
            # Loop through table.

            while true
                line = fptr.readline

                line = unencrypt_str(line) if encrypted
                line.strip!

                # If blank line (i.e. 'deleted'), skip it.

                next if line == ''

                # Split the line up into fields.

                rec = line.split('|', @col_poss.max+2)

                append_new_rec_to_index_array(rec)
            end
        # Here's how we break out of the loop...

        rescue EOFError
        end
    
        @last_update = Time.new
    end
    
    #-----------------------------------------------------------------------

    # add_index_rec

    #-----------------------------------------------------------------------

    def add_index_rec(rec)
        @last_upddate = Time.new if append_new_rec_to_index_array(rec)
    end

    #-----------------------------------------------------------------------

    # delete_index_rec

    #-----------------------------------------------------------------------

    def delete_index_rec(recno)
        i = @idx_arr.rassoc(recno.to_i)
        @idx_arr.delete_at(@idx_arr.index(i)) unless i.nil?
        @last_update = Time.new
    end

    #-----------------------------------------------------------------------

    # update_index_rec

    #-----------------------------------------------------------------------

    def update_index_rec(rec)
        delete_index_rec(rec.first.to_i)
        add_index_rec(rec)
    end

    #-----------------------------------------------------------------------

    # append_new_rec_to_index_array

    #-----------------------------------------------------------------------

    def append_new_rec_to_index_array(rec)
        idx_rec = []
        @col_poss.zip(@col_types).each do |col_pos, col_type|
            idx_rec << convert_to_native_type(col_type, rec[col_pos])
         end

        return false if idx_rec.uniq == [kb_nil]

        idx_rec << rec.first.to_i
        @idx_arr << idx_rec
        return true
    end
end


#---------------------------------------------------------------------------

# KBRecnoIndex

#---------------------------------------------------------------------------

class KBRecnoIndex
    include KBEncryptionMixin

    #-----------------------------------------------------------------------

    # initialize

    #-----------------------------------------------------------------------

    def initialize(table)
        @idx_hash = {}
        @table = table
    end

    #-----------------------------------------------------------------------

    # get_idx

    #-----------------------------------------------------------------------

    def get_idx
        return @idx_hash
    end

    #-----------------------------------------------------------------------

    # rebuild

    #-----------------------------------------------------------------------

    def rebuild(fptr)
        @idx_hash.clear

        encrypted = @table.encrypted?

        begin
            # Skip header rec.

            fptr.readline

            # Loop through table.

            while true
                # Record current position in table.  Then read first

                # detail record.

                fpos = fptr.tell
                line = fptr.readline

                line = unencrypt_str(line) if encrypted
                line.strip!

                # If blank line (i.e. 'deleted'), skip it.

                next if line == ''

                # Split the line up into fields.

                rec = line.split('|', 2)

                @idx_hash[rec.first.to_i] = fpos
            end
        # Here's how we break out of the loop...

        rescue EOFError
        end
    end

    #-----------------------------------------------------------------------

    # add_index_rec

    #-----------------------------------------------------------------------

    def add_index_rec(recno, fpos)
        raise 'Table already has index record for recno: %s' % recno if \
         @idx_hash.has_key?(recno.to_i)
        @idx_hash[recno.to_i] = fpos
    end

    #-----------------------------------------------------------------------

    # update_index_rec

    #-----------------------------------------------------------------------

    def update_index_rec(recno, fpos)
        raise 'Table has no index record for recno: %s' % recno unless \
         @idx_hash.has_key?(recno.to_i)
        @idx_hash[recno.to_i] = fpos
    end

    #-----------------------------------------------------------------------

    # delete_index_rec

    #-----------------------------------------------------------------------

    def delete_index_rec(recno)
        raise 'Table has no index record for recno: %s' % recno unless \
         @idx_hash.has_key?(recno.to_i)
        @idx_hash.delete(recno.to_i)
    end
end


#---------------------------------------------------------------------------

# KBTableRec

#---------------------------------------------------------------------------

class KBTableRec
    include KBTypeConversionsMixin

    def initialize(tbl)
        @tbl = tbl
    end

    def populate(rec)
        @tbl.field_names.zip(rec).each do |fn, val|
            send("#{fn}=", val)
        end
    end

    def clear
        @tbl.field_names.each do |fn|
            send("#{fn}=", kb_nil)
        end
    end
end


#---------------------------------------------------------

# KBResultSet

#---------------------------------------------------------------------------

class KBResultSet < Array
    #-----------------------------------------------------------------------

    # KBResultSet.reverse

    #-----------------------------------------------------------------------

    def KBResultSet.reverse(sort_field)
        return [sort_field, :desc]
    end

    #-----------------------------------------------------------------------

    # initialize

    #-----------------------------------------------------------------------

    def initialize(table, filter, filter_types, *args)
        @table = table
        @filter = filter
        @filter_types = filter_types
        super(*args)

        @filter.each do |f|
            get_meth_str = "def \#{f}()\nif defined?(@\#{f}) then\nreturn @\#{f}\nelse\n@\#{f} = self.collect { |x| x.\#{f} }\nreturn @\#{f}\nend\nend\n"
            self.class.class_eval(get_meth_str)
        end
    end

    #-----------------------------------------------------------------------

    # to_ary

    #-----------------------------------------------------------------------

    def to_ary
        to_a
    end

    #-----------------------------------------------------------------------

    # set

    #-----------------------------------------------------------------------

    #++

    # Update record(s) in table, return number of records updated.

    #

    def set(*updates, &update_cond)
        raise 'Cannot specify both a hash and a proc for method #set!' \
         unless updates.empty? or update_cond.nil?

        raise 'Must specify update proc or hash for method #set!' if \
         updates.empty? and update_cond.nil?

        if updates.empty?
            @table.set(self, update_cond)
        else
            @table.set(self, updates)
        end
    end

    #-----------------------------------------------------------------------

    # sort

    #-----------------------------------------------------------------------

    def sort(*sort_fields)
        sort_fields_arrs = []
        sort_fields.each do |f|
            if f.to_s[0..0] == '-'
                sort_fields_arrs << [f.to_s[1..-1].to_sym, :desc]
            elsif f.to_s[0..0] == '+'
                sort_fields_arrs << [f.to_s[1..-1].to_sym, :asc]
            else
                sort_fields_arrs << [f, :asc]
            end
        end

        sort_fields_arrs.each do |f|
            raise "Invalid sort field" unless @filter.include?(f[0])
        end

        super() { |a,b|
            x = []
            y = []
            sort_fields_arrs.each do |s|
                if [:Integer, :Float].include?(
                 @filter_types[@filter.index(s[0])])
                    a_value = a.send(s[0]) || 0
                    b_value = b.send(s[0]) || 0
                else
                    a_value = a.send(s[0])
                    b_value = b.send(s[0])
                end
                if s[1] == :desc
                    x << b_value
                    y << a_value
                else
                    x << a_value
                    y << b_value
                end
            end

            x <=> y
        }
    end

    #-----------------------------------------------------------------------

    # to_report

    #-----------------------------------------------------------------------

    def to_report(recs_per_page=0, print_rec_sep=false)
        result = collect { |r| @filter.collect {|f| r.send(f)} }

        # How many records before a formfeed.

        delim = ' | '

        # columns of physical rows

        columns = [@filter].concat(result).transpose

        max_widths = columns.collect { |c|
            c.max { |a,b| a.to_s.length <=> b.to_s.length }.to_s.length
        }

        row_dashes = '-' * (max_widths.inject {|sum, n| sum + n} +
         delim.length * (max_widths.size - 1))

        justify_hash = { :String => :ljust, :Integer => :rjust,
         :Float => :rjust, :Boolean => :ljust, :Date => :ljust,
         :Time => :ljust, :DateTime => :ljust }

        header_line = @filter.zip(max_widths, @filter.collect { |f|
            @filter_types[@filter.index(f)] }).collect { |x,y,z|
                 x.to_s.send(justify_hash[z], y) }.join(delim)

        output = ''
        recs_on_page_cnt = 0

        result.each do |row|
            if recs_on_page_cnt == 0
                output << header_line + "\n" << row_dashes + "\n"
            end

            output << row.zip(max_widths, @filter.collect { |f|
                @filter_types[@filter.index(f)] }).collect { |x,y,z|
                    x.to_s.send(justify_hash[z], y) }.join(delim) + "\n"

            output << row_dashes + '\n' if print_rec_sep
            recs_on_page_cnt += 1

            if recs_per_page > 0 and (recs_on_page_cnt ==
             num_recs_per_page)
                output << '\f'
                recs_on_page_count = 0
            end
        end
        return output
    end
end


#---------------------------------------------------------------------------

# KBNilClass

#---------------------------------------------------------------------------

class KBNilClass 
    include Comparable

    class << self
        def new
            @kb_nil ||= KBNilClass.allocate
        end
    end

    def inspect
        'kb_nil'
    end

    def kb_nil?
        true
    end

    def to_s
        ""
    end

    def to_i
        0
    end

    def to_f
        0.0
    end

    def to_a
        []
    end

    def <=>(other)
        return 0 if other.kb_nil?
        return -1
    end

    def coerce(other)
        return [other, to_i] if other.kind_of? Fixnum
        return [other, to_f] if other.kind_of? Float

        raise "Didn't know how to coerce kb_nil to a #{other.class}"
    end

    def method_missing(sym, *args)
        kb_nil
    end
end


#---------------------------------------------------------------------------

# Kernel

#---------------------------------------------------------------------------

module Kernel
    def kb_nil
        KBNilClass.new
    end
end


#---------------------------------------------------------------------------

# Object

#---------------------------------------------------------------------------

class Object
    def full_const_get(name)
        list = name.split("::")
        obj = Object
        list.each {|x| obj = obj.const_get(x) }
        obj
    end

    def kb_nil?
        false
    end
end


#---------------------------------------------------------------------------

# Symbol

#---------------------------------------------------------------------------

class Symbol
    #-----------------------------------------------------------------------

    # -@

    #-----------------------------------------------------------------------

    #

    # This allows you to put a minus sign in front of a field name in order

    # to specify descending sort order.

    def -@
        ("-"+self.to_s).to_sym
    end

    #-----------------------------------------------------------------------

    # +@

    #-----------------------------------------------------------------------

    #

    # This allows you to put a plus sign in front of a field name in order

    # to specify ascending sort order.

    def +@
        ("+"+self.to_s).to_sym
    end
end