build_header_field_string (KirbyBase)

    # 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