=============================================================
                      Custom Records
=============================================================

INTRODUCTION:

One of the new features provided by version 3.0 of the
Paradox Engine and Database Framework is the concept of
Custom Records. This document describes how to create and
use Custom Records, including advanced features such as data
validation and field mapping. Custom Records can only be used
with C++ or Pascal with the Database Framework.

At the most basic level, Custom Records map the fields of
a table to variables within the Custom Record class. This
simplifies the housekeeping required to access data in a
Paradox table. These variables can be directly accessed
without using the getField or putField methods ( the field
number of the variable is not required ). Reading records
from a table will automatically load the variables into the
Custom Record, while writing data to the table will
automatically get the data from the variables.

The generate.exe utility will automatically create a
custom record from a table, generating a <tablename>.h file
and a <tablename>.cpp file. The .h file contains the class
definition and the .cpp file contains the method definitions.

SIMPLE EXAMPLE:

Create a table called people in either Paradox or using
the maketbl.cpp example ( which is supplied at the end of
this document ) with the following table structure:

    - First Name ( A20 )
    - Last Name  ( A20 )
    - Age        (  N  )
    - Date       (  D  )
    - Note       ( M40 )

Use the generate utility to create the Custom Record:

    generate /c people

View the people.h file. Notice that the 'people' class
contains the following data members:

    char First_Name[21];
    char Last_Name[21];
    double Age;
    BDate       Date;
    BLOBHANDLE  Note;
    char nullVec[1];

The nullVec member is used as a flag to determine if each
field is NULL. The generate utility will create a nullVec
member large enough to have one bit per member of the
Custom Record. The nullVec member is described later in
this section.

The First_Name, Last_Name, Age, Date, and Note members
correspond to the fields of the same name in the table.
All non-BLOb ( Binary Large OBject ) fields ( Numeric,
Currency, Short, and Date ) can be directly modified:
data written to the variables will automaticaly be
written to the table when the Custom Record is saved.

BLOb fields are not directly accessible in the same manner.
Custom records allow a lower level of control over BLOb
fields than Generic Records by providing access to the
BLOBHANDLE. The BLOBHANDLE is used with the standard C
functions to access a BLOb field. The getFieldNumber()
method can also be used to get the FIELDNUMBER for use with
the BRecord methods for accessing BLOb's. The file
memfld.zip, available on the local BBS at ( 408 ) 439-9096
is an example of using BLOb fields from within the Database
Framework.

Custom Records make use of the nullVec variable to
determine whether a field is NULL or not. The actual
data member is not modifed when a NULL field is read
from the disk: only the corresponding bit in the nullVec
variable is set.

Failing to respect the nullVec member can lead to two
sorts of problems: Displaying a NULL field, and updating
a field. The variable corresponding to the NULL field
will contain whatever data it contained before the record
was read from the table, resulting in invalid data being
displayed: use the isNull() method to test the field
before displaying it. Modifying a NULL field and saving
the record will not work because the Database Framework
will not write NULL fields to disk, resulting in no
changes being made to that field: Use the clearNull()
method to mark the field as being used ( not NULL ).
Custom Records also provide the setNull() ( mark the
field as NULL ) and the clear() ( Mark all fields as
NULL ) methods for modifying the nullVec variable.

The following example shows how a non-BLOb field is
modified, taking into consideration nullVec:

    BEngine myEngine( pxLocal );
    BDatabase myDataBase( &myEngine ) ;
    BCursor myCursor( &myDataBase, "people" ) ;
    people table( &myCursor ) ;

    myCursor.gotoBegin() ; // Before first record
    myCursor.gotoNext() ; // First record in table
    // Which loads the members of table
    // We have to test if the table is NULL
    myCursor.getRecord( &table ) ;
    if( !table.isNull( "First_Name" )
    {
        cout << table.First_Name << endl ;
    } else { // Mark field as not being NULL
        table.clearNull( "First_Name" ) ;
    }
    // Modify the first field
    strcpy( table.First_Name, "Testing" ) ;
    // Post the changes to disk
    myCursor.updateRec( &table ) ;

A test was added to determine if the retrieved data member
is NULL because the data members of a Custom Record are not
modified if the field is marked as NULL. If the 'First Name'
field is NULL on disk, the value in First_Name will be the
same as it was before the call to myCursor.getRecord(). This
also means that any changes made to the First_Name member
will not be stored on disk as long as the field is marked as
NULL. We call the clearNull() to mark the field as used so
that our change gets saved to the table.


=============================================================
                      Deriving Classes
=============================================================

The 'people' class currently does not take advantage of the
full potential of Custom Records. Functionality is added by
deriving a new class and overriding some methods of the
people class.

First, the housekeeping. At a minimum the derived class will
need a constructor and a new nameOf method:

    class mypeople : public people {
	public:
        mypeople( BCursor *cursor ) : people( cursor ) {};
        char *nameOf() const { return( "mypeople" ) } ;
	} ;

The nameOf() method is used internally by the Database
Framework to determine, for example, if the current class
is a Custom Record or a Generic Record.

It is recomended to add a method, set(), which will mark
all fields as being used. This way one method can be
called when all variables in the custom record are being
used.

DATA VALIDATION:

Functionality is added by overriding the preprocess() and
postprocess() methods. The preprocess() method is called
whenever the record is written to the table using the
BCursor methods updateRec(), insertRec, and appendRec().
preprocess() is called internally before the data is
written. The postprocess() method is called whenever the
record is retrieved from the disk using the BCursor method
getRecord(). postprocess() is called after the data is read
from the table. This is where a Custom Record would handle
any data validation.

It would be nice for the mypeople Custom Record to
automatically handle NULL fields. This can be done by
modifying the postprocess method:

    Retcode people::postprocess()
    {
        if (isNull( "First_name" ) {
            strcpy( First_Name, "" ) ;
            clearNull( "First_Name" ) ;
        }
        return(lastError = PXSUCCESS);
    }

Now the 'First_Name' field is set to a defined value even if
the field is NULL on disk. The return value will not prevent
the data members from being set, but it will return the
error value to the calling method ( one of the BCursor
methods ) for the programmer to detect and handle ).

Similar tests can be performed in the preprocess() method:

    Retcode people::preprocess()
    {
        // test if the 'First_Name' field contains ""
        if ( strcmp ( First_Name, "" ) )
        {
            cerr << endl ;
            cerr << "Must give a valid First Name" ;
            cerr << endl ;
            return ( lastError = PXERR_OUTOFRANGE ) ;
            // Any error message can be used....
        }
        return(lastError = PXSUCCESS);
    }

This will ensure that the 'First_Name' field contains
a value before the record is written from disk. Returning an
error from the preprocess method will prevent the record
from being written to the table.

These methods can also be used to determine if the value in
the field is a certain value or within a certain range of
values.


=============================================================
                     Advanced Features:
=============================================================

MAPPING FIELDS:

The generate utility can also use a "map" file, which allows
the explicit mapping of a field in the table to a field
in the Custom Record. The data type of the variable within
the Custom Record can be different from the data type of the
field, the variable can have a name different from the field
name, and "derived" fields can be created which do not
correspond to any fields in the table. This section
describes how to use these features.

The map file contains the name of the Custom Record, the name
of the table, the data type for each member of the Custom
Record, the variable name for each member, and the name of
the field in the table. The format of the mapfile is:

    <Recordname> from <Tablename> (
        <Datatype>    <Variablename<size> > : <Fieldname>,
        <Datatype>    <Variablename<size> > : <Fieldname>
                                  )

Recordname and Variablename can be any identifier that is not
a C or C++ keyword. Tablename needs to be the name of a valid
Paradox table in the current directory.

The Datatype can be any valid C, C++, Paradox Engine, or user
defined type. Note that the generate utility will give a
warning for types that it does not understand ( such as a user
defined type ), but the Custom Record will still be created.
It is the programmers responsibility to make certain that the
unknown types are declared in the Custom Record class ( i.e.,
include the correct header files ). The Database Framework
will automatically make certain conversions, but it is the
programmer's responsibility to make certain that the field
given by Fieldname can be converted to the specified
Datatype ( the DBF uses the convertFld function in
BRecord.cpp ):

		 To: | Char | Short | Long | Double | Date |
	      -------|------|-------|------|--------|-------
	From: Char   |  X   |   X*  |  X*  |   X*   |  X   |
	      Short  |  X   |   X   |  X   |   X    |      |
	      Long   |  X   |   X*  |  X   |   X    |      |
	      Double |  X   |   X*  |  X*  |   X    |      |
	      Date   |  X   |   x*  |      |        |  X   |

			* Only if argument in Range.

Fieldname can be either a valid fieldname in the given table,
or it can be "", for "derived" field, to indicate that the
member of the Custom Record does not directly map to a
field in the table.

The mapfile for the above example would look like this:

    people from people (
        char        First_Name[21] :  "First Name",
        char        Last_Name[21]  :  "Last Name",
        double      Age            :  "Age",
        BDate       Date           :  "Date",
        BLOBHANDLE  Note           :  "Note"
                       )

The /f switch is provided with the generate utility to read
a map file:

    generate /c /f people.map

The map file can be changed to incorporate different fields
and data types. First off, Age is currently stored as a
double, which is slightly overkill. This can be changed to
an INT16 using a mapfile ( INT16 is defined in envdef.h ).
There are also cases when it is desirable to have two fields
in the table combined into one field in the Custom Record.
For example, a single member which contains both the first
and the last name. This can be done by adding a new member
to the Custom Record class, "Name", which does not map
directly to any field in the table ( using "" as it's field
type ). Lastly, the name to the "Note" field will be changed
to "description". This is a new Custom Record, so it deserves
a new name, newCust:

    newCust from people (
       char       First_Name[21] : "First Name",
       char       Last_name[21]  : "Last Name",
       char       Name[42]       : "",
       INT16      Age            : "Age",
       BDate      Date           : "Date",
       BLOBHANDLE Description    : "Note"
			)


'generate /c /f newcust.map' creates a newcust.h and a
newcust.cpp from the 'people' table containing the custom
record newCust. The postprocess() method has to be modified
to load the 'Name' variable. For this example 'First_Name'
needs to be copied to 'Name', a blank needs to be inserted
into 'Name', and then 'Last_Name' needs to be concatenated
to the end of 'Name':

    RetCode newCust::postprocess()
    {
	strcpy( Name, First_Name ) ;
	strcat( Name, " " ) ;
	strcat( Name, Last_Name ) ;
	return 0 ;
    }

This same general method is used to create other types of
derived fields, such as structures and unions.

=============================================================
                   Example Files:
=============================================================

//********************* MAKETBL.CPP *************************
//                Used to create the table.
//  Create a project file containing: maketbl.cpp, pxmsg.cpp,
//               pxengtcl.lib, and dbfeng.lib.

#include <iostream.h>
#include <alloc.h>

#include <pxengine.h>
#include <envdef.h>
#include <bengine.h>
#include <bdatabas.h>

#define TABLENAME "people"
const int numFields = 5 ;
FieldDesc desc[numFields] = { // Define the fields
     { 1, "First Name", fldChar,   fldstNone, 20 },
     { 2, "Last Name",  fldChar,   fldstNone, 20 },
     { 3, "Age",        fldDouble, fldstNone,  0 },
     { 4, "Date",       fldDate,   fldstNone,  0 },
     { 5, "Note",       fldBlob,   fldstMemo, 40 } } ;

int main( void )
{
   BEngine myEngine( pxLocal ) ;
   if ( !myEngine.lastError ) {
      BDatabase myDataBase( &myEngine ) ;
      if ( !myEngine.lastError ) {
      myDataBase.createTable( TABLENAME, numFields,
			      desc ) ;
      }
   }
   return 0 ;
} //**************** end maketbl.cpp ************************



