BUILDING A DATABASE FILE VIEWER

Programming with the Paradox Engine

Michael Floyd

Michael is executive editor for Dr. Dobb's Journal. He can be reached at the DDJ offices, on CompuServe at 76703,4057, or via MCI mail at mfloyd.


A database engine is a library that provides--in the form of an API--the core features of a database management system. One advantage of an engine is that you can use routines that have been fully optimized and tested to add database functionality to an application. A database engine also allows your application to support a common data format, so that there's no need to develop a proprietary one. This means other applications or tools can access data created by your program, and vice versa. And, while many applications are not databases per se, they often have sophisticated data-storage and -retrieval requirements--CD-ROM applications, for example, usually require an optimized search engine just to access files.

This article presents a data-file viewer written with Borland's Paradox Engine 3.0 database engine and Borland Pascal 7.0 with Objects. The DOS-based viewer provides both a means of exploring the Paradox Engine and a useful tool for development efforts. Although the application uses Borland Pascal, much of the discussion is relevant to both C and C++ programmers working under DOS or Windows. In fact, any Windows developer whose development platform can access DLLs can use the techniques presented here.

Touring the Engine

The Paradox Engine supports Borland's C/C++ and Pascal compilers for both DOS and Windows.

DOS programmers can also access engine functions using Microsoft C 6.01 (or later). Because the Paradox Engine is supplied as a dynamic link library (DLL), Windows programmers can access the functions from any tool that supports DLLs--Visual Basic, Toolbook, ObjectVision, and the like. In addition to the procedural libraries, the Paradox Engine comes with a Database Framework--an object-oriented library that encapsulates the functionality of the engine and gives C++ and Pascal programmers high-level access to Paradox Engine functions.

Compared to previous releases of the Paradox Engine, version 3.0 supports binary large objects (BLOBs), improves on concurrency support, and no longer requires you to specify a network type when initializing the engine. Although the Paradox Engine does not currently support dBase file formats (or at least importing dBase data), 3.0 does support Paradox 3.5 and 4.0 file formats, allowing you to open and manipulate both 3.5- and 4.0-formatted files, as well as import 3.5 files to the new format.

The memory requirements of the Paradox Engine are negligible. Under DOS, for example, you can compile using Borland's overlay manager to swap chunks of up to 90 Kbytes in and out of RAM. I was able to code and test the database viewer presented here from within the integrated development environment (IDE). It was only when I tried to add a Turbo Vision interface that memory requirements forced me to use the command-line compiler. (This also becomes an issue when working with the Database Framework.) However, if you're using Borland Pascal 7.0 and a 386, you can take advantage of the new DPMI support. Windows is less of an issue since most Windows environments already take advantage of DPMI. Consequently, the Engine-as-DLL can be shared between applications, thus making efficient use of memory.

The Engine at Work

There's a classic chicken-and-egg problem in the first stages of database development. On one hand, the mechanisms to manipulate and view data are either not yet in place or not fully debugged. On the other hand, maintaining the integrity of data is critical in testing new functions. Therefore, you may be tempted to first build the viewer functions. However, opening a database on, for example, a composite index does not provide an accurate view of the physical data. In fact, most database applications present views of the database that do not represent its current state. The solution is to build a generic data viewer that can quickly display the database.

Listing One (page 28) presents a viewer that can read and display any Paradox database table that's been described by a "structure file"--a plain ASCII file that describes the fields for a given table. There are a number of benefits to using structure files: They document the fields in your database, allow generic routines to access any Paradox database table, and make it possible to view Paradox data without your having to own Paradox. Table 1 describes the syntax used in the structure file to describe Paradox field types. For example, an ASCII field containing a maximum of 20 characters is described as A20. One field type not supported by the file viewer is the BLOB type. While it is a simple matter to read and display, say, an integer, BLOB fields can contain a memo field, a bitmap image, or even sound data. Therefore, access routines must be written to read and write each specific BLOB type.

Table 1: Syntax used to describe field types in a table-structure file.

  Syntax  Field         Description
  _________________________________________________________________________

  Annn    Alphanumeric  nnn defines the maximum number of characters.  A50
                        defines an alphanumeric field that contains a
                        maximum of 50 characters.

  N       Numeric       Double-precision floating-point value.

  S       Short Number  Signed-integer value from -32,767 to 32,767.

  D       Date          Valid dates using the Gregorian calendar from
                        January 1, 100 to December 31, 9999.  Date fields
                        can be viewed in any of five different formats
                        and printed using one of eight additional formats.

  $       Currency      Similar to Numeric field, but by default displays
                        values rounded to two decimal places and places
                        negative numbers in parentheses.

If you're familiar with versions 2 or 3 of the Paradox Engine, you'll notice some familiar code in Listing One. Some of the support routines such as Strip, Error, LoadTableStructure, and InputRecord were "borrowed" from the Fondex example program supplied with the Paradox Engine. (This example provided inspiration for the generic database viewer.)

To use the viewer, create a new table by first creating a structure file using your favorite editor, then firing up the viewer and selecting "New Table." The viewer program then calls NewTable to load the table-structure file and creates a new table by calling the PXTblCreate engine function. A call to KeyAdd is also included, but has been commented out. KeyAdd creates a primary index based on the field specified. To create a primary index, simply uncomment the call to KeyAdd.

Once a table has been created, you can add a new record, delete or update existing data, and search the data table. There are also options to open and close a table. These two options are important. Because although you've created a new table, it has yet to be opened, and a table must be opened before you can perform any operations on it. OpenStruct, the procedure that actually opens the table, first ensures that it is not already open. The Paradox Engine doesn't require this step. In fact, the engine allows for up to 64 file handles and related objects (such as indexes) to be open at any one time. If you plan on supporting multiple tables, you should store FirstTableOpen (which is a Boolean value) in some dynamic structure such as an array. Opening as many tables as possible is probably not a good practice however. Using PXTblClose to close a table flushes the buffers, thus freeing them for use elsewhere in the program and guaranteeing the update of disk files in a timely fashion. With that in mind, the total number of open files is by default set to five. If you need additional tables, this default value can be changed using PXSetDefaults prior to calling one of the PXInit functions.

With the issue of open tables out of the way, OpenStruct calls the PXTblOpen engine function. As one of its arguments, PXTblOpen takes an index ID value that specifies the order for records in the table. In this case the ID value is set to 0, which opens the table in the order of the primary index if one exists, or in natural order if there's no primary index. If the table is successfully opened, a call to PXRecBufOpen allocates a record buffer for the table. GetTableStructure is then called to retrieve the table's field names and types. The "Close Table" option is provided for completeness. As previously mentioned, PXTblClose automatically flushes the buffers and should be used whenever possible to free up table handles and update to disk. You can, however, safely exit the viewer without closing a currently open table--the engine automatically closes all open tables before exiting.

Once a table is open, you can begin to add data to it using the AddEntry function in Listing One. As with OpenStruct, AddEntry first checks for an open table and exits with an error message if that's the case. AddEntry also flushes the record buffer with a call to the PXRecBufEmpty engine function. InputRecord then gets the field values from the user and places them in the record buffer. Finally, PXRecAppend performs the update to the database.

Searching

Searching with the Paradox Engine is straightforward, requiring only a single call to perform most searches. PXSrchFld takes a table, a record, and a field handle as arguments along with a forth Mode parameter. Mode specifies the PXSrchFld search mode. Valid modes are SEARCHFIRST, SEARCHNEXT, and CLOSESTRECORD. For example, consider the Search procedure in Listing One. PXSrchFld is placed in a loop with Mode initialized to SEARCHFIRST. If no matches are found, a message is displayed. Otherwise, the results are read into the record buffer using PXRecGet and displayed. If the user chooses to search for additional matches, Mode is set to SEARCHNEXT and the loop repeats.

By default, searches are case sensitive and the viewer relies on this. You can create a search that is insensitive to case by creating an index for the field or fields (for a composite index) to search on (PXKeyAdd), opening the table on this index (PXTblOpen), and calling PXSrchFld as usual. Note, too, that the rules change when you open a table without specifying an index. As previously mentioned, PXTblOpen opens the table in the order of the primary index if one exists, or in natural order if there is no primary index. PXSrchFld uses the natural order when the table is not indexed and the CLOESESTRECORD mode is not valid.

Database Framework

The Database Framework is a class library that encapsulates the Engine's API. The idea is that providing a set of high-level objects will reduce the complexity of application development. However, Paradox is not an object-oriented DBMS. The Database Framework simply provides an abstraction of Paradox tables into a virtual table using the functionality of Paradox.

All of the source code used to build the Database Framework is included with the Engine in both Pascal and C++. There are, however, significant differences between the Pascal and C++ versions. In particular, the C++ Framework defines a base object, BDbObject, from which all other database engine objects are derived; see Figure 1(a). The C++ Framework is designed in such a way that it is independent of Turbo Vision or the Object Windows Library (OWL), allowing you to use your favorite GUI class library.

The base object in the Pascal version, on the other hand, is TObject--the base object used in both Turbo Vision and OWL. Four Engine objects are derived from TObject: TEngine, TDatabase, TCursor, and TRecord; see Figure 1(b). According to Borland, using TObject as the base object in the database hierarchy allows them to make use of TCollection to store dynamic collections of records. Agreed, there's not a plethora of third-party application frameworks available for Turbo Pascal. However, this strategy of inheriting from TObject ties you into an application framework that you may never use.

Still, the Database Framework simplifies several aspects of Pascal programming. For example, the Windows API requires null-terminated strings (defined as PChar in Turbo Pascal), while the DOS API allows the use of the standard Turbo String type. On the other hand, both the DOS and Windows versions of the Database Framework use String variables. The Database Framework thus resolves the difference in string handling and allows even Windows programmers to work with the simpler Turbo Pascal String type.

Returning to the hierarchy in Figure 1(b), you'll notice the TEngine object type. TEngine is to the Database Framework what TApplication is to Turbo Vision or OWL. There is one and only one instance of TEngine, and it is the first object instantiated. TEngine methods handle basic initialization of the engine (in Windows, single-user, or network mode), allowing you to set and get defaults and handle things like password protection.

Once you've initialized the engine, you can create and open a database table, which is handled within the framework by the TDatabase object type. TDatabase methods simplify operations on tables by treating the table as a whole and by allowing you to reference table names rather than handles. There are methods to create, copy, rename, and report on tables. In creating a table, you can establish primary and secondary indexes, and you can easily open tables on simple, composite, or case-insensitive indexes. Methods to navigate the table come from TCursor. The TCursor object type encapsulates routines for accessing indexed and unordered tables. More than one cursor can be open on the same table, thus allowing multiple views of a given table.

Finally, the TRecord object type handles reading from and writing to record fields, including BLOB fields. A TRecord is always created in the context of a TCursor. Most notable about TRecord is its encapsulation of what Borland calls a "generic record," a record whose structure is not known until run time. In addition to generic records, you can also create custom records. These custom records, which are derived from TRecord, allow you to create alternative views of the record data by combining some or all existing fields with derived fields. Also, with custom records, you can directly map record fields to variables in your program. This allows you to update fields without having to call GetField and PutField.

There is one slight hitch in using custom records: A custom record class must override some of TRecord's methods. Fortunately, Borland supplies a Generate utility that, when given a table name and list of source and target fields, will generate a custom-record object type (or class in the case of C++). This custom record will be derived from TRecord.

Concurrency

You don't have to be on a network to consider file sharing in your application. Windows, although non-preemptive, is still a multitasking operating system. As such, you can open multiple instances of an application that can, in turn, simultaneously lead to multiple accesses to a given table. Even DOS now provides a file SHARE mode which can cause problems. Therefore, the database developer must always be cognizant of those issues generally associated with networks. The 12 network API function calls include: record, table, and file locking; table refreshing, error handling; and a Goto function and allows you to go to a previously locked record.

For those of you on a network, Paradox Engine 3.0 supports Novell Netware, 3Com, 3Com3+Open, IBM PC LAN, AT&T StarGroup, Banyan Vines, local-share net types, and other DOS 3.1 compatible networks. Prior to the 3.0 version of the engine, the developer had to specify which network the application was to run. This was not a big deal, but it did force the developer to add some #ifdef statements. Under 3.0, however, you no longer have to specify the network--the Paradox Engine handles it for you.

Conclusion

There is one small problem I've noticed in deleting records from the database apps built with the Paradox Engine. Whenever you delete an entry in the database, PXRecDelete removes the pointers to that record, effectively marking the data for overwriting. But this operation does not actually delete the data. As a result, your database can grow much larger than the apparent number of records in the database. Therefore, you'll need to write a packing function that creates a new table with the same structure as your current table and copies only active records to this new table. According to Borland, packing a database with a primary index and then adding records requires a significant amount of table reorganization as the new records are inserted. Borland provides a technical-information note (TI1004) that describes the problem and its solution. A copy of the technical note is available on CompuServe in the Borland Development Tools forum (GO DBEVTOOLS) as TI1004.ASC.


_BUILDING A DATABASE FILE VIEWER_
by Michael Floyd


[LISTING ONE]

{$N+,E+}
Program DbViewer;
{ DbViewer is a simple Paradox table viewer. DbViewer can display most, but
  not all, data field types. DbViewer includes options to create a new
  table, open, close and search existing tables, and to add, edit and
  display records in a given table. Note: this program was adapted from
  the FONDEX example program included with the Paradox Engine. }
Uses DOS, Crt, PXEngine, PXMSG;

Const
  Success = True;
  Failure = False;
  MaxFields = 40;
  MaxFieldSize = 50;
Var
  RecHandle                                       : RecordHandle;
  NdxHandle, TblHandle                            : TableHandle;
  FirstTableOpen, SecondTableOpen, ThirdTableOpen : Boolean;
  TextFile                                        : Text;
  NFields                                         : Integer;
  Names, Types                                    : NamesArrayPtr;
  StructureFile, DataFile                         : String;

Type
  IntArray = array[1..18] of Integer;

{--- Strip a string of leading and trailing white space ---}
procedure Strip(var S: String );
  var
    L1, L2: Byte;
  begin
    L1 := 1;
    while (L1 < Length(S)) and (S[L1] in [#9..#13, ' ']) do
      Inc(L1);
    L2 := Length(S);
    while (L2 > 0) and (S[L2] in [#9..#13, ' ']) do
      Dec(L2);
    S := Copy(S, L1, L2 - L1 + 1);
  end; { Strip }

{--- Write error message if an error has occurred ---}
function Error(RC: Integer): Boolean;
  begin
    if RC <> PXSUCCESS then
      WriteLn('DbViewer: ', PXErrMsg(RC));
    Error := RC <> PXSUCCESS;
  end; { Error }

{--- trap error, but ignore ---}
procedure ErrIgnore(RC: Integer);
  begin
    if Error(RC) then; { ignore error return code }
  end; { ErrIgnore }

{--- Load a table structure from an ASCII disk file ---}
function LoadTableStructure: Boolean;
  var
    F: Text;
    FldName, FldType, Help: String;
  begin
    { Open the structure file }
    Assign(F, StructureFile);
{$I-}
    Reset(F);
{$I+}
    if IoResult <> 0 then
      begin
        WriteLn('can''t open Structure file');
        LoadTableStructure := FAILURE;
        Exit;
      end;
    { Read in the structure }
    NFields := 0;
    New(Names);
    New(Types);
    while not Eof(F) and (NFields < MAXFIELDS) do
    begin
        { read data, bewaring of unexpected EOF }
{$I-}
        ReadLn(F, Help);
{$I+}
        if (IoResult = 0) and (Help <> '') then
        begin
            Inc(NFields);
            Strip(Help);
            FldType := Copy(Help, 1, Pos(' ', Help) - 1);
            FldName := Copy(Help, Pos(' ', Help), Length(Help));
            { remove trailing and leading white space from name }
            Strip(FldName);
            Names^[NFields] := FldName;
            Types^[NFields] := FldType;
          end; { THEN }
      end; { WHILE }
    Close(F);
    { Return error if no fields were found }
    if NFields = 0 then
      LoadTableStructure := FAILURE
    else
      LoadTableStructure := SUCCESS;
  end; { LoadTableStructure }

{--- Frees memory associated with table structure ---}
procedure FreeTableStructure;
  begin
    Dispose(Names);
    Dispose(Types);
  end; { FreeTableStructure }

{--- Retrieve table field names and types ---}
function GetTableStructure(THandle: TableHandle) : Boolean;
  var
    FldName, FldType: NameString;
    I: Word;
  begin
    NFields := 0;
    New(Names);
    New(Types);
    if Error(PXRecNFlds(THandle, NFields)) then; { ignore Result }
    for I := 1 to NFields do
      if not Error(PXFldName(THandle, I, FldName)) and
         not Error(PXFldType(THandle, I, FldType)) then
        begin
          Names^[I] := FldName;
          Types^[I] := FldType;
        end
      else
        begin
          GetTableStructure := FAILURE;
          Exit;
        end;
    GetTableStructure := SUCCESS;
  end; { GetTableStructure }

procedure ShowFiles(Filename : String);
var
  I : Integer;
  Found : SearchRec;
begin
  I := 1;
  Assign(TextFile, Filename);
  {$I-}
  Reset(TextFile);
  {$I+}
  While IOResult <> 0 do
  begin
    ClrScr;
    Writeln('-------- Available Files ----------');
    FindFirst('*.dat', AnyFile, Found);
    While DosError = 0 do
    begin
      Writeln(Found.Name);
      FindNext(Found);
    end;
    Writeln;
{    Write('Enter Filename (less extension): ');
    Readln(Filename);}
  end;
end;

{--- Open a structure file and associated table ---}
procedure OpenStruct(StructType : String; var THandle : TableHandle);
var
  FileN, S : String;
begin
    { Attempt to open the table and allocate a record buffer. If table
       is open, inidicate an error }
  If StructType = 'author' then
    if FirstTableOpen then
    begin
      WriteLn('table already opened');
      Exit;
    end;
{ uncomment this section to support additional tables }
{  If StructType = 'foo' then
    if SecondTableOpen then
    begin
      WriteLn('table already opened');
      Exit;
    end;
  If StructType = 'bar' then
    if ThirdTableOpen then
    begin
      WriteLn('table already opened');
      Exit;
    end;
}
    { Now try and open the table }
    if Error(PXTblOpen(StructType, TblHandle, 0, False)) then
      Exit;
    { Allocate a record buffer }
    if Error(PXRecBufOpen(TblHandle, RecHandle)) then
      Exit;
    if GetTableStructure(TblHandle) = FAILURE then
      Exit;
    THandle := TblHandle;
    If StructType = 'author' then FirstTableOpen := True;

{ uncomment this section to support additional tables }
{    If StructType = 'foo'  then SecondTableOpen := True;
    If StructType = 'bar' then ThirdTableOpen := True;
}
  end; { OpenStruct }

{--- Close the table if opened ---}
procedure CloseStruct(TblName : String; THandle : TableHandle);
  begin
  If TblName = 'author' then
     if not FirstTableOpen then
     begin
       WriteLn('table not open');
       Exit;
     end;
{ uncomment this section to support additional tables }
{  If TblName = 'foo' then
     if not SecondTableOpen then
     begin
       WriteLn('table not open');
       Exit;
     end;
  If TblName = 'bar' then
    if not ThirdTableOpen then
     begin
       WriteLn('table not open');
       Exit;
     end;
}
    { Free the record buffer }
    if Error(PXRecBufClose(RecHandle)) then
      Exit;
    { Close the table }
    if Error(PXTblClose(THandle)) then
      Exit;
    FreeTableStructure;
      If TblName = 'author' then  FirstTableOpen := False;

{ Uncomment this to support additional tables}
{     If TblName = 'foo'   then SecondTableOpen := False;
      If TblName = 'bar' then ThirdTableOpen := False;
}
  end; { CloseStruct }

{--- Retrieve in a string format any valid Paradox type ---}
function GetData(FH: FieldHandle;
                 var S: String ): Boolean;
 var
    TheDate: TDate;
    Month, Day, Year: Integer;
    TheValue: Double;
    TheShort: Integer;
    IsBlank: Boolean;
    Help: String ;
  begin
    { if this field is blank, we want to return a blank string }
    GetData := SUCCESS;
    if not Error(PXFldBlank(RecHandle, FH, IsBlank)) then
      if IsBlank then
        S := ''
      else
        case UpCase(Types^[FH][1]) of
          'A':
            if Error(PXGetAlpha(RecHandle, FH, S)) then
              GetData := FAILURE;
          'D':
            if not Error(PXGetDate(RecHandle, FH, TheDate)) then
              begin
                ErrIgnore(PXDateDecode(TheDate, Month, Day, Year));
                Str(Month, S);
                Str(Day, Help);
                S := S + '/' + Help;
                Str(Year, Help);
                S := S + '/' + Help;
              end
            else
              GetData := FAILURE;
          'N':
            if not Error(PXGetDoub(RecHandle, FH, TheValue)) then
              Str(TheValue: 5: 0, S)
            else
              GetData := FAILURE;
          '$':
            if not Error(PXGetDoub(RecHandle, FH, TheValue)) then
              Str(TheValue: 6: 2, S)
            else
              GetData := FAILURE;
          'S':
            if not Error(PXGetShort(RecHandle, FH, TheShort)) then
              Str(TheShort, S)
            else
              GetData := FAILURE;
        end { case }
    else
      GetData := FAILURE; { an error occured in PXFldBlank }
  end; { GetData }

{--- Store a string in any valid Paradox type ---}
function PutData(FH: FieldHandle;
                 S: String ): Boolean;
var
    TheDate: TDate;
    Month, Day, Year: Integer;
    TheValue: Double;
    TheShort: Integer;
    Code: Integer;                      { needed for VAL }

  function GetNextWVal(var S: String ): Word;
    const
      Delim = '/';
    var
      L: Byte;
      Help: Word;
      Code: Integer;
    begin
      L := Pos(Delim, S);
      if L = 0 then
        L := Length(S) + 1;
      Val(Copy(S, 1, L - 1), Help, Code);
      S := Copy(S, L + 1, Length(S));
      if Code = 0 then
        GetNextWVal := Help
      else
        GetNextWVal := 0;
    end; { GetNextWVal }
  begin
    PutData := SUCCESS;
    case UpCase(Types^[FH][1]) of
      'A':
        if Error(PXPutAlpha(RecHandle, FH, S)) then
          PutData := FAILURE;
      'D':
        begin
          Month := GetNextWVal(S);
          Day := GetNextWVal(S);
          Year := GetNextWVal(S);
          if Error(PXDateEncode(Month, Day, Year, TheDate)) or
             Error(PXPutDate(RecHandle, FH, TheDate)) then
            PutData := FAILURE;
        end;
      '$', 'N':
        begin
          Val(S, TheValue, Code);
          if Error(PXPutDoub(RecHandle, FH, TheValue)) then
            PutData := FAILURE;
        end;
      'S':
        begin
          Val(S, TheShort, Code);
          if Error(PXPutShort(RecHandle, FH, TheShort)) then
            PutData := FAILURE;
        end;
    end; { case }
  end; { PutData }

{--- Edit existing record buffer and let user accept, cancel, or re-edit ---}
function InputRecord: Boolean;
  var
    C: Char;
    I: Word;
    Buf: String ;
  begin
    InputRecord := FAILURE;
    { Keep attempting to input until user selects DONE or CANCEL }
    while True do
      begin
        { Go through all fields }
        for I := 1 to NFields do
          begin
            { translate the current value into the input buffer }
            if GetData(I, Buf) <> SUCCESS then
              Exit;
            WriteLn(Buf);
            { ask for the new value }
            Write(Names^[I], ': ');
            ReadLn(Buf);

      { Now translate it back into the record buffer unless old value
         is kept by just hitting return. }
            if Length(Buf) > 0 then
              if PutData(I, Buf) <> SUCCESS then
                Exit;
          end; { for }
        { Ask what to do with this input }
        WriteLn('S)ave, C)ancel, R)edo:');
        repeat
          C := UpCase(ReadKey);
        until C in ['S', 'C', 'R'];
        case C of
          'S':
            begin
              InputRecord := SUCCESS;
              Exit;
            end;
          'C': Exit;
        end; { case }
      end; { while }
  end; { InputRecord }

{--- Add a new record to the table ---}
procedure AddEntry;
  begin
    if not FirstTableOpen then
      begin
        WriteLn('Table not opened');
        Exit;
      end;
    { Empty the current record buffer }
    if Error(PXRecBufEmpty(RecHandle)) then
      Exit;
    { get the fields unless input is cancelled by user }
    if InputRecord = FAILURE then
      Exit;
    { Attempt to append the record }
    ErrIgnore(PXRecAppend(TblHandle, RecHandle));
  end; { AddEntry }

{--- Displays and accepts a legal field number ---}
function InputField(var FieldNumber: FieldHandle): Boolean;
  var
    Buf: String ;
  begin
    { Get the field number as an integer }
    FieldNumber := Ord(ReadKey) - Ord('0');
    if (FieldNumber < 1) or (FieldNumber > NFields) then
      begin
        WriteLn('illegal field number');
        InputField := FAILURE;
        Exit;
      end;

    { Input the field }
    Write(Names^[FieldNumber], ': ');
    ReadLn(Buf);
    { And translate it }
    if PutData(FieldNumber, Buf) <> SUCCESS then
      begin
        InputField := FAILURE;
        Exit;
      end;
    InputField := SUCCESS;
  end; { InputField }

procedure FindAndUpdate;
  begin
    if InputRecord <> FAILURE then { Update it }
      if PXRecUpdate(TblHandle, RecHandle) <> PXSUCCESS then
        Exit;
  end; { FindAndUpdate }

{--- Processes search allowsing user to Delete and Update records ---}
procedure DoSearch(FieldNumber: FieldHandle);
  var
    Mode: Integer;
    I: Integer;
    Done: Boolean;
    Buf: String ;
  begin
    Mode := SEARCHFIRST;
    Done := True;
    while True do
      begin
        { If no match found, get out }
        if PXSrchFld(TblHandle, RecHandle, FieldNumber,
           Mode) <> PXSUCCESS then
          begin
            WriteLn('No Matches');
            Exit;
          end;
        { Get the record found }
        if Error(PXRecGet(TblHandle, RecHandle)) then
          Exit;
        { Print the record }
        for I := 1 to NFields do
          begin
            if GetData(I, Buf) <> SUCCESS then
              Exit;
            WriteLn(Names^[I], ': ', Buf);
          end;
        WriteLn('N)ext, D)elete, U)pdate, E)xit Search:');
        repeat
          case UpCase(ReadKey) of
            'N':
              begin { Search for the next occurrence }
                Mode := SEARCHNEXT;
                Done := True;
              end;
            'D':
              begin
                ErrIgnore(PXRecDelete(TblHandle));
                Exit;
              end;
            'U':
              begin
                FindAndUpdate;
                Exit;
              end;
            'E': Exit;
            else Done := False;
          end; { case }
        until Done;
      end; { while }
  end; { DoSearch }

{--- Search functions ---}
procedure Search;
  var
    FieldNumber, I: Word;
  begin
    if not FirstTableOpen then
      begin
        WriteLn('table not open');
        Exit;
      end;
    { List the fields to search on }
    WriteLn('Select Field');
    for I := 1 to NFields do
      WriteLn(I, ' ', Names^[I]);
    { Get the input field to search on }
    if InputField(FieldNumber) = FAILURE then
      Exit;
    { Perform Search Options }
    DoSearch(FieldNumber);
  end; { Search }

procedure NewTable(Filename : String);
begin
  if LoadTableStructure = Failure then
    Writeln('NewTable: Cannot open table')
  else
    if not Error(PXTblCreate(DataFile, NFields, Names, Types)) then
      FreeTableStructure;
end;

procedure KeyAdd;
var
  FldHandles : FieldHandleArray;

begin
  FldHandles[1] := 1;   FldHandles[2] := 2;
  If not Error(PXKeyAdd(DataFile, 2, FldHandles, Primary)) then
    Writeln('Key field inserted');
end;

procedure Menu;
var
  C : Char;
begin
  StructureFile := 'author.dat';
  DataFile   := 'author';
  repeat
    Writeln;
    Writeln('1 - New Table');
    Writeln('2 - Open Table');
    Writeln('3 - Add Entry');
    Writeln('4 - Search Entry');
    Writeln('5 - Close Table');
    Writeln('6 - Quit');
    C := ReadKey;
    Case C of
      '1' : Begin
              Writeln('Enter File Name to Save As: ');
              Readln(StructureFile);
              NewTable(StructureFile);
{              KeyAdd;}
            end;
      '2' : Begin
              ShowFiles(DataFile);
              OpenStruct(DataFile, NdxHandle);
            end;
      '3' : AddEntry;
      '4' : Search;
      '5' : CloseStruct('indextbl', NdxHandle);
      '6' : ;
      else
        Writeln('Invalid Option');
    end;
  until
      C = '6';
end;

{--- Main ---}
Var
  Val : Integer;
  Ndx : Real;
  Done : Boolean;
  Ans : Char;
  PxErr : Integer;
Begin

  ClrScr;
  FirstTableOpen := False;
  PXErr := PXSetDefaults(32, 5, 10, 1, 10, SortOrderAscii);
  PxErr := PxInit;
  If PxErr = PxSuccess then
    Menu
  Else
  begin
    Writeln('problem');
    readln;
    Halt(1);
  end;
  PX(PXExit);
End.






Copyright © 1993, Dr. Dobb's Journal