The FairCom DB API API record management hides from the user all the complexities of maintaining a generic record buffer for tables. While fixed-length records may be easier to implement and maintain, variable-length records present a greater challenge for the developer.
When a table contains variable-length records, each record read may require buffers of different sizes. The FairCom DB API record manager performs the function of expanding and shrinking record buffers to deal with the necessities of variable-length records. The FairCom DB ISAM API also requires different API calls for fixed-length and variable-length records, but the FairCom DB API presents to the user one common API that deals with both fixed- and variable-length records.
Only the application architecture and system resources limit the number of FairCom DB API record buffers associated with a table.
Creating a record object
A valid record object is required before most record operations can take place. You need to pass a valid CTTable object to the CTRecord constructor.
// create a CTRecord object
CTRecord ARecord(ATable);
If you create a dynamic CTRecord object with the new operator, you are required to destroy the object with the delete operator.
// create a dynamic CTRecord object
CTRecord* pRecord = new CTRecord(ATable);
if (!pRecord)
{
printf("CTRecord creation failed\n");
}
... other operations ..
// destroy the CTRecord object
delete pRecord;
After the record object has been created, but before any record operations can take place, the table associated with the record object must be opened.
Record buffer layout
Based on the fields added by the user, FairCom DB API will organize the record buffer as follows:
$DELFLD$ |
$NULFLD$ |
$ROWID$ |
field 0 |
field 1 |
... |
field n |
The user fields are stored in the table record in the same order in which they were added.. To initiate the operation on the records, a record handle must be declared and instantiated. Once the table associated with the record handle is opened, the record handle can be used to store the information that come from or go to the fields in the table.
Sharing the same context
Every time one of the CTRecord class constructors creates a record object, the record object acquires its own context, so that each record buffer operates independently. Record operations that move the current record position will not interfere with each other.
There are situations where it may be necessary to have several different record objects sharing the same record context. In this case the developer can create the first record buffer using one of the CTRecord class constructors to acquire a new context. The initial record can then be duplicated by creating a new record object and passing the original record object instead of a table object. The duplicated records will share the same context of the original record buffer, but the duplicated record handle will not share the same memory buffer for holding the record data.
// create two records sharing the same context
CTRecord ARecord1(ATable);
// create the duplicated record object
CTRecord ARecord2(ARecord1);
In the code fragment above, the record object ARecord1 was created passing a table object as the parent object. ARecord1 is created with a new record context. ARecord2 was created passing the ARecord1 object as the parent object. ARecord2 is a duplicated copy of ARecord1.
Resetting a record
A record object may be reset to its initial state, i.e. to the same state it had just after being created. The record reset function will perform the following actions on a record handle:
- The context associated with the record is released
- Record sets are turned off
- Memory allocated for record buffer is released
- Record properties are reinitialized.
// reset a record
try
{
ARecord.Reset();
}
catch (CTException &err)
{
printf("Record reset failed with error %d\n", err.GetErrorCode());
}
All records associated with a table can be reset in one operation by calling CTTable::ResetAll() method.
// reset all records
try
{
ATable.ResetAll();
}
catch (CTException &err)
{
printf("Reset all records failed with error %d\n", err.GetErrorCode());
}
When a table is closed, all records associated with that table are automatically reset by the CTTable::Close() method.
The default index
Some of the record operations such as record navigation, finding records and record sets require an index to be specified. The default index property is used by the record functions that required an index to operate on.
Any of the user defined indexes, or RECBYT, or ROWID, in the case these are created, may be used as the index controlling the search. To set the default index use one of the overloaded CTRecord::SetDefaultIndex() methods. You can use the index number of the index name to identify the default index.
// create a new record object
CTRecord ARecord(ATable);
//set the default index to "MyIndex"
try
{
ARecord.SetDefaultIndex("MyIndex");
}
catch (CTException &err)
{
printf("Set default index failed with error %d\n", err.GetErrorCode());
}
Use CTRecord::GetDefaultIndex() to retrieve the current index number or CTRecord::GetDefaultIndexName() to retrieve the name of the current default index.
// make sure the default index is set to "MyIndex"
try
{
if (ARecord.GetDefaultIndexName() != "MyIndex")
{
ARecord.SetDefaultIndex("MyIndex");
}
}
catch (CTException &err)
{
printf("Set default index failed with error %d\n", err.GetErrorCode());
}
If no index is set as the default index before the first search, the default index is the first user-defined index. Once an index is chosen as the default index, all subsequent searches will be done using this index, up to a new selection in the default index. For instance: the user may set the default index as the ROWID index, search for the first input record, then change the default index to the user defined index (i_name, for instance, an index over the name field in the table), and search for the next record on this particular sequence, taking as the previous record the first input record.
A user may perform a chronological search in the records in a table using CTRecord::First() and CTRecord::Next(), and setting the ROWID index as the default index:
Selecting the RECBYT index
The RECBYT index can be selected as the default index in order to perform record navigation with CTRecord::First(), CTRecord::Last(), CTRecord::Next() and CTRecord::Prev(), ordered by the record offset. The RECBYT index can be selected as the default index by passing the index value of CTDB_RECBYT_IDXNO to CTRecord::SetDefaultIndex() method.
// set the RECBYT index as the default index
try
{
ARecord.SetDefaultIndex(CTDB_RECBYT_IDXNO);
}
catch (CTException &err)
{
printf("Set default index failed with error %d\n", err.GetErrorCode());
}
Selecting the ROWID index
The ROWID index can be selected as the default index in order to perform record navigation with CTRecord::First(), CTRecord::Last(), CTRecord::Next() and CTRecord::Prev(), ordered by the ROWID value of each record. The ROWID index can be selected as the default index by passing the index value of CTDB_ROWID_IDXNO to CTRecord::SetDefaultIndex() method.
// set the ROWID index as the default index
try
{
ARecord.SetDefaultIndex(CTDB_ROWID_IDXNO);
}
catch (CTException &err)
{
printf("Set default index failed with error %d\n", err.GetErrorCode());
}
Navigating records
FairCom DB API provides features that allow navigation among records of a particular table. You can position the current record at the first record, last record, next record, previous record, or seek to a specific record if you know the record offset.
Setting a different default index for the record may change the order of the record navigation.
The record navigation function not only updates the current position of a record, but the record data is read from disk and placed in the record buffer inside the record handle. As the record is read from disk, the new record flag is set to false to indicate that the data in the record buffer originated from an existing record in the table.
First record
Call CTRecord::First() to position the current record at the first record of a table. If the table is empty, i.e. the table contains no records, CTRecord::First() returns NO (false).
// get the first record of the table
try
{
if (ARecord.First())
printf("First record found\n");
else
printf("Table is empty\n");
}
catch (CTException &err)
{
printf("First record failed with error %d\n", err.GetErrorCode());
}
Last record
Call CTRecord::Last() to position the current record at the last record of a table. If the table is empty, i.e. the table contains no records, CTRecord::Last() returns NO (false).
// get the last record of the table
try
{
if (ARecord.Last())
printf("Last record found\n");
else
printf("Table is empty\n");
}
catch (CTException &err)
{
printf("Last record failed with error %d\n", err.GetErrorCode());
}
Next record
Call CTRecord::Next() to position the current record at the next record of a table. If the current record is already the last record of a table, CTRecord::Next() returns NO (false).
// get the next record of the table
try
{
if (ARecord.Next())
printf("Next record found\n");
else
printf("No more records\n");
}
catch (CTException &err)
{
printf("Next record failed with error %d\n", err.GetErrorCode());
}
A current record must exist before CTRecord::Next() is called or an ICUR_ERR (100) is thrown indicating that there can be no next record is the current record does not exist. You can establish a current record by calling CTRecord::First(), CTRecord::Last(), CTRecord::Find() or CTRecord::SeekRecord().
Previous record
Call CTRecord::Prev() to position the current record at the previous record of a table. If the current record is already the first record of a table, CTRecord::Prev() returns NO (false).
// get the previous record of the table
try
{
if (ARecord.Prev())
printf("Previous record found\n");
else
printf("Already at first record\n");
}
catch (CTException &err)
{
printf("Prev record failed with error %d\n", err.GetErrorCode());
}
A current record must exist before CTRecord::Prev() is called, or an ICUR_ERR (100)exception is thrown indicating that there can be no previous record if the current record does not exist. You can establish a current record by calling CTRecord::First(), CTRecord::Last(), CTRecord::Find() or CTRecord::SeekRecord().
Seek to record
If you know in advance the offset of a given record, CTRecord::SeekRecord() can be used to make it the current record.
You can use CTRecord::GetRecordPos() and CTRecord::SeekRecord() to implement a bookmark system for your records. Retrieve the record offset with CTRecord::GetRecordPos(), which is the bookmark value, and later on you can quickly go back to the record by calling CTRecord::SeekRecord().
// get record bookmark
CTOFFSET GetBookMark(CTRecord &ARecord)
{
return ARecord.GetRecordPos();
}
// goto record bookmark
void GotoBookmark(CTRecord &ARecord, CTOFFSET bookmark)
{
ARecord.SeekRecord(bookmark);
}
Finding records
You can search for records in a table using one of the CTRecord::Find(), CTRecord::FindTarget() and CTRecord::FindRowid() methods. FairCom DB API performs the find operations against the table indexes. When an index entry is found, the record data is loaded into the record buffer.
Before you can call CTRecord::Find(), you need to prepare the data you want to find:
- Clear a record buffer
- Set the default index with the appropriate value
- Populate the fields that makeup the index segment
- Call CTRecord::Find() with the appropriate find mode
// find record which product code is DISKETTE
CTRecord ARecord(ATable);
// clear the record buffer
ARecord.Clear();
// set the default index to "MyIndex"
ARecord.SetDefaultIndex("MyIndex");
// populate the 'product' field
ARecord.SetFieldAsString("product", "DISKETTE");
// find the record
try
{
if (ARecord.Find(CTFIND_EQ))
printf("Record found\n");
else
printf("Record not found\n");
}
catch (CTException &err)
{
printf("Record find failed with error %d\n", err.GetErrorCode());
}
CTRecord::Find(), CTRecord::FindTarget() and CTRecord::FindRowid() returns YES (true) if the record was found or NO (false) if the record was not found. These methods throw an exception if an error occurs.
Find Modes
Use the following find modes with the record find methods:
FairCom DB API |
FairCom DB API .NET |
Explanation |
|---|---|---|
CTFIND_EQ |
EQ |
Find a record equal to the target |
CTFIND_LT |
LT |
Find a record less than target |
CTFIND_LE |
LE |
Find a record less or equal than target |
CTFIND_GT |
GT |
Find a record greater than target |
CTFIND_GE |
GE |
Find a record greater or equal than target |
Note: The Find Mode CTFIND_EQ requires that the target contains values for all segments that compose the index and the index cannot allow duplicates.
Note: FairCom DB API .NET defines this mode with the FIND_MODE enum.
Building a target key
A target key can be built using most of the steps used to find a record using CTRecord::FindTarget() method. To build a target key you must:
- Clear a record buffer
- Set the default index with the appropriate value
- Populate the fields that makeup the index segment
- Call CTRecord::BuildTargetKey() with the appropriate find mode and the buffer to receive the target key
// build a target key
CTRecord ARecord(ATable);
TEXT key[256];
VRLEN keylen = sizeof(key);
// clear the record buffer
ARecord.Clear();
// set the default index to index 0
ARecord.SetDefaultIndex(0);
// populate the 'product' field
ARecord.SetFieldAsString("product", "DISKETTE");
// build the target key
try
{
ARecord.BuildTargetKey(CTFIND_EQ, key, &keylen);
}
catch (CTException &err)
{
printf("Build target key failed with error %d\n", err.GetErrorCode());
}
Finding records by target key
A record can also be found by passing a target key already built by calling CTRecord::FindTarget() method. CTRecord::FindTarget() takes as its parameters a pointer to a target key and the find mode.
// find record with a target key
CTRecord ARecord(ATable);
TEXT key[256];
VRLEN keylen = sizeof(key);
// clear the record buffer
ARecord.Clear();
// set the default index to index 0
ARecord.SetDefaultIndex(0);
// populate the 'product' field
ARecord.SetFieldAsString("product", "DISKETTE");
// build the target key
ARecord.BuildTargetKey(CTFIND_EQ, key, &keylen);
try
{
if (ARecord.FindTarget(key, CTFIND_EQ))
printf("Record found\n");
else
printf("Record not found\n");
}
catch (CTException &err)
{
printf("Find target failed with error %d\n", err.GetErrorCode());
}
Finding records by ROWID
A record can be quickly located if the ROWID for that record is known by calling CTRecord::FindRowid(). CTRecord::FindRowid() take as its parameters the ROWID value and the find mode.
// find record with rowid of 1000
CTRecord ARecord(ATable);
try
{
if (ARecord.FindRowid(1000, CTFIND_EQ))
printf("Record found\n");
else
printf("No record found\n");
}
catch (CTException &err)
{
printf("Find Rowid failed with error %d\n", err.GetErrorCode());
}
Record Sets
An alternative and more efficient way to retrieve records is the use of Record Sets. A record set contains part of a table, representing a group of records that have keys that match a certain target. For example, suppose an index contains the following keys:
CASE DISKCASE DISKDRIVE DISKETTE KEY KEYBOARD KEYCAP
When scanning through the index looking for records, record navigation functions work on the entire index file. Navigating to the first record obtains the first key in the index, CASE, and moving to the next record obtains each of the following keys in turn, on through KEYCAP.
When creating a record set using a record set function, with a target of "DISK" and a target length of 4, FairCom DB API returns only the records in which the first 4 bytes of the key start with "DISK".
The record navigation functions will operate only on the records that match the set until the record set operation is terminated with a call to a record set function. To work with multiple record sets at once, use more than one record handle.
Record sets allow a faster search for records, particularly in client/server mode since fewer records may be retrieved and less traffic on the network will be generated. When used in conjunction with filters (see below), one can create simple queries.
To use record sets, follow the steps below:
- Clear the record buffer
- Set the index you want to use
- Populate the index segments you would like to participate in the set
- Establish the Set
Creating a record set
To create a record set, perform the following steps
- Clear a record handle
- Select the default index
- Populate the field, or fields, that form the partial index
- Call CTRecord::RecordSetOn() to create a record set
// create a new record object
CTRecord ARecord(ATable);
// clear the record buffer
ARecord.Clear();
// populate the fields
ARecord.SetFieldAsString(0, "DISK");
// create the record set
try
{
ARecord.RecordSetOn(4);
}
catch (CTException &err)
{
printf("Record set on failed with error %d\n", err.GetErrorCode());
}
Once the record set is created, CTRecord::First(), CTRecord::Next(), CTRecord::Last() and CTRecord::Prev() will return only the records that match the record set criteria.
Terminating a record set
Once the record set is no longer necessary, you can turn it off by calling CTRecord::RecordSetOff().
// terminate the record set
try
{
ARecord.RecordSetOff();
}
catch (CTException &err)
{
printf("Record set off failed with error %d\n", err.GetErrorCode());
}
Once the record set is terminated, CTRecord::First(), CTRecord::Next(), CTRecord::Last() and CTRecord::Prev() will return all records of a table.
Record Filters
FairCom DB API allows users to define record filters using CTTable::FilterRecord(). When a filter is set, all records retrieved from a table are filtered against the given expression, and only records matching filter criteria are returned.
// set filter
try
{
ATable.FilterRecord("ZIPCODE == 12345");
}
catch (CTException &err)
{
printf("Filter record failed with error %d\n", err.GetErrorCode());
}
Only the user who sets the filter will have its records filtered. The filter is turned off when the table is closed, or when CTTable::FilterRecord() is called with an empty string as the filter expression. Only one filter may be active per table per user at once, so if a new filter is set to a table with an existing filter for the specific table, the old filter will be overridden.
// terminate filter
try
{
CTString empty;
ATable.FilterRecord(empty);
}
catch (CTException &err)
{
printf("Filter record failed with error %d\n", err.GetErrorCode());
}
Use CTTable::GetFilter() to retrieve the current filter expression. If no filters are active, CTTable::GetFilter() return an empty string. Use CTTable::IsFilteredRecord() to test if filters are active for a table.
When used in the client/server model, this feature has the potential to increase the performance since just the records matching the criteria will be returned, reducing the network traffic.
Record filters, when used in conjunction with sets provide the FairCom DB API API with simple query capabilities.
FairCom DB Expression Parser and Grammar
A powerful expression parser/analyzer provides for complex conditional expressions that can be defined and evaluated at runtime.
Filter expression syntax closely follows the C language syntax for expressions, including order of precedence. An expression interpreted by the expression parser should compile without errors with a standard C compiler. As in C, you cannot compare strings directly like LastName > 'S'. However, the expression parser has a number of built-in functions that allow the comparison of strings. Example:
strcmp( LastName, "S" ) > 0
The expression handling assures proper alignment considerations are handled, and ensures buffer size of any record being evaluated is big enough.
Routines that evaluate conditional expressions maintain fixed data record lengths and total data record lengths. This permits correct alignment adjustments and detects if insufficient data is available. The latter condition results in a CVAL_ERR (598) error. The easiest way to produce a CVAL_ERR (598) is to read only the fixed-length portion of a data record, and have an expression that relies on fields in the variable-length portion of the record.
For additional control, a Conditional Expression Callback Function is available. This allows advanced control through an external user created function.
See also
- Data Filters
- Conditional Index Support
- Partitioned Files
- cndxparse
- cndxeval
- cndxrun
- cndxfree
- ctparsedoda
- getcndxmem
- putcndxmem
Constants
The expression parser uses the constants below internally as a 32-bit signed or unsigned integer:
- Character constants: any valid char enclosed in single quotes, e.g., 'a'
- Signed Integer constants: values from - 2,147,438,647 to 2,147,438,647
- Unsigned Integer constants: values from 0 to 4,294,967,295
- Hexadecimal constants: values from 0x00000000 to 0xffffffff. Any combination of lower case or upper case letters are accepted, e.g., 0Xbc4f or 0xF5C56d
- Date constants:
{d'yyyy-[m]n-[d]d'}
yyyy is 4 digit year
[m]m is 1 or 2 digit month
[d]d is 1 or 2 digit day
Examples:
{d'2024-12-06'}
{d'2024-8-6'}
'[m]m-[d]d-yyyy' or '[m]m/[d]d/yyyy'
yyyy is 4 digit year
[m]m is 1 or 2 digit month
[d]d is 1 or 2 digit day
Examples:
'12-06-2024'
'8-6-2024'
'8/6/2024'
'yyyy-[m]m-[d]d' or 'yyyy/[m]m/[d]d'
yyyy is 4 digit year
[m]m is 1 or 2 digit month
[d]d is 1 or 2 digit day
Examples:
'2024-12-06'
'2024-8-6'
'2024/8/6'
'[d]d-mmm-yyyy' or '[d]d/mmm/yyyy'
[d]d is 1 or 2 digit day
mmm is 3 letter month name
yyyy is 4 digit year
month names (case insensitive) are:
JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
Examples:
'1/jan/2024'
'12-feb-2024'
- Time constants:
{t'[h]h:[m]m:[s]s'}
[h]h is 1 or 2 digit hour
[m]m is 1 or 2 digit minute
[s]s is 1 or 2 digit second
Examples:
{t'23:11:33'}
{t'1:2:3'}
'[h]h:[m]m:[s]s[:[i][i]i]'
[h]h is 1 or 2 digit hour
[m]m is 1 or 2 digit minute
[s]s is 1 or 2 digit second
[i][i]i is optional 1 to 3 digit millisecond
Examples:
'1:2:3'
'12:33:44.123'
- Timestamp constants:
{ts'yyyy-[m]m-[d]d [h]h:M[M]:s[s]'}
yyyy is 4 digit year
[m]m is 1 or 2 digit month
[d]d is 1 or 2 digit day
[h]h is 1 or 2 digit hour
[M]M is 1 or 2 digit minute
[s]s is 1 or 2 digit second
Examples:
{ts'2024-08-12 22:33:44'}
{ts'2024-08-12 2:3:4'}
'[m]m-[d]d-yyyy [h]h:[M]M:[s]s[:[i][i]i]' or '[m]m/[d]d/yyyy [h]h:[M]M:[s]s[:[i][i]i]'
[m]m is 1 or 2 digit month
[d]d is 1 or 2 digit day
yyyy is 4 digit year
[h]h is 1 or 2 digit hour
[M]M is 1 or 2 digit minute
[s]s is 1 or 2 digit second
[i][i]i is optional 1 to 3 digit millisecond
Examples:
'08-12-2024 22:33:44'
'08/12/2024 2:3:4.123'
'yyyy-[m]m-[d]d [h]h:[M]M:[s]s[:[i][i]i]' or 'yyyy/[m]m/[d]d [h]h:[M]M:[s]s[:[i][i]i]'
yyyy is 4 digit year
[m]m is 1 or 2 digit month
[d]d is 1 or 2 digit day
[h]h is 1 or 2 digit hour
[M]M is 1 or 2 digit minute
[s]s is 1 or 2 digit second
[i][i]i is optional 1 to 3 digit millisecond
Examples:
'2024-08-12 22:33:44'
'2024/08/12 2:3:4.123'
'[d]d-mmm-yyyy [h]h:[M]M:[s]s[:[i][i]i]' or '[d]d/mmm/yyyy [h]h:[M]M:[s]s[:[i][i]i]'
[d]d is 1 or 2 digit day
mmm is 3 letter month name
yyyy is 4 digit year
[h]h is 1 or 2 digit hour
[M]M is 1 or 2 digit minute
[s]s is 1 or 2 digit second
[i][i]i is optional 1 to 3 digit millisecond
month names (case insensitive) are:
JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
Examples:
'08-dec-2024 22:33:44'
'08/dec/2024 2:3:4.123'
Any integer larger than the maximum size allowed for integers, or any number with decimal points, or any numbers in scientific notation are interpreted as a floating point constant by the expression parser .
String constants are similar to C string constants and represent any text enclosed by double quotes, for example, "This is a string". The maximum size of a string constant defaults to 255 characters. The filter expression parser allows the following escape characters in string constants or character constants:
Escape char |
Value |
Explanation |
|---|---|---|
\a or \A |
ASCII 7 |
bell |
\b or \B |
ASCII 8 |
backspace |
\f or \F |
ASCII 12 |
Form Feed |
\n or \N |
ASCII 10 |
Linefeed |
\r or \R |
ASCII 13 |
Carriage Return |
\t or \T |
ASCII 9 |
tab |
\v or \V |
ASCII 11 |
vertical tab |
\\ |
|
|
\any |
|
Any character not listed above |
Variables
A filter expression variable is actually the name of the fields defined for the table. There is a limit of 128 characters for the name of variables and the names are case sensitive.
When a user specifies a variable name, the filter parser searches the table definition for a field of that name. The parser uses the type of the field and converts it to the types used internally by the expression evaluator. The conversion of field types is as follows:
Field Type |
Data Type |
Field Type |
Data Type |
|---|---|---|---|
CT_BOOL |
int |
CT_SFLOAT |
double |
CT_CHAR |
int |
CT_DFLOAT |
double |
CT_CHARU |
unsigned |
CT_FSTRING |
char* |
CT_INT2 |
int |
CT_FPSTRING |
char* |
CT_INT2U |
unsigned |
CT_F2STRING |
char* |
CT_INT4 |
int |
CT_F4STRING |
char* |
CT_INT4U |
unsigned |
CT_STRING |
char* |
CT_DATE |
unsigned |
CT_PSTRING |
char* |
CT_TIME |
unsigned |
CT_2STRING |
char* |
CT_MONEY |
int |
CT_4STRING |
char* |
Please note that "int" is a LONG, "unsigned" is a ULONG and "char*" is a pTEXT.
Field names that match a valid expression reserved word:
Consider a field named "year", which collides with the function YEAR. The expression "[year] == 2000" is needed to handle "year" as a field name rather than the function.
Parentheses
Use parentheses exactly like they are used in C expressions. There are no limits on the number of parentheses you may use in an expression, as long as each open parenthesis has a closing parenthesis. Parentheses are also used to enclose the arguments of built-in functions.
Predefined Functions
The FairCom DB conditional expression parser has numerous built-in functions for advanced conditional filtering possibilities. They are described in the Predefined Functions section of the FairCom DB Developer's Guide.
Type Casting
The filter expression parser allows you to use explicit type casts in expressions. This is very useful if you are comparing fields of different types and want to control the result of an expression.
For example, suppose "Salary" is a CT_MONEY field and "Average" is a CT_DFLOAT field; type casts can be used as illustrated in the following expression: (Salary - (int)Average) > 500
The following type casts may be used in conditional expressions:
- (int) or (long): Convert the result of expression to integer (32 bit).
- (unsigned [int | long]): Convert the result of expression to unsigned integer (32 bit).
- (double): Convert the result of expression to double.
You cannot type cast a string expression.
Automatic Type Promotion
When mixing different types in an expression without explicit type casting, the conditional expression parser automatically promotes the types using the following rule:
- signed and unsigned integers - promoted to unsigned integer (64-bit)
- signed integer and double - promoted to double
- unsigned integer and double - promoted to double
In the great majority of cases, mixing strings with numeric values returns a parsing error.
Operators
The following operators are allowed in filters and conditional expressions.
Mathematical Operators
+ |
Adds two operands |
- |
Subtracts two operands or negates an operand (e.g., -5) |
* |
Multiplication |
/ |
Division |
% |
Modulus |
Relational Operators
== |
Equal to |
!= |
Not equal to |
< |
Less than |
<= |
Less or equal to |
> |
Greater than |
>= |
Greater than or equal to |
Logical Operators
&& |
And |
|| |
Or |
! |
Not |
Binary Operators
& |
And |
| |
Or |
~ |
Not |
^ |
Xor |
NULL Operators
IS NULL |
IS NOT NULL |
Custom Application Expressions
Use the conditional expression parser/analyzer to evaluate application-specific expressions. The FairCom DB expression parser is a full standalone feature in and of itself, and can be used directly in your own applications for purposes other than data filters, conditional indexes, or partitioned files. Two core API functions provide a powerful interface into this advanced expression handling.
FairCom DB’s expression parser/analyzer requires two key steps:
- Call cndxparse() to parse your expression, producing an expression tree that the expression analyzer later evaluates.
- Call cndxeval() to evaluate your expression tree using data from a buffer in memory.
For a complete sample program, see ctexpr.c in the ctree/source directory.
See also:
Parsing Expressions
Parsing your expression involves three steps:
- Define a DODA structure.
- Parse the DODA into a record schema and field name list.
- Parse your expression to produce an expression tree.
Sample code to perform these steps is shown below. This code assumes FairCom DB has been initialized prior to calling ctparsedoda().
Expression Parsing Example
#include "ctcndx.h" /* For PTREE type */
/* Define a DODA structure. */
DATOBJ doda[] = {
{"CustomerNumber", 0, CT_INT4U},
{"ZipCode", 4, CT_FSTRING, 9},
{"State", 13, CT_FSTRING, 2},
{"LastName", 15, CT_STRING, 37},
{"FirstName", 52, CT_STRING, 37},
{"Address", 89, CT_STRING, 49},
{"City", 138, CT_STRING, 37}
};
COUNT retval; /* Return code. */
pTEXT schema; /* Record schema. */
pTEXT names; /* Field name list. */
PTREE ptree; /* Expression tree. */
pTEXT expr; /* Expression string. */
/* Parse the DODA into a record schema and field name list. */
if ((retval = ctparsedoda(doda, 7, &schema, &names)) != 0)
printf("Error %d parsing DODA.\n", retval);
/* Parse your expression to produce an expression tree. */
expr = "stricmp(LastName, \"Smith\") == 0
&& CustomerNumber > 10000";
ptree = cndxparse(schema, names, expr, strlen(expr));
if (!ptree)
printf("Error: Unable to parse expression.\n");
else
printf("Successfully parsed expression.\n");
To invoke the expression parser to parse a string expression and produce an expression tree, call cndxparse(), which is declared as follows:
PTREE cndxparse(pConvMap Schema, pTEXT Names, pTEXT InputText,
NINT InputTextSize)
Schema is a pointer to a record schema derived from a DODA definition. Names is a pointer to a list of the field names from the DODA. InputText points to a NULL-terminated string expression, and InputTextSize is the length of InputText.
One of the most useful features of the expression parser is its ability to associate symbolic names in expressions with data in a buffer in memory. To use this ability, you must define a record schema, known as a DODA (Data Object Definition Array). A DODA is an array of field specifications, each of which contains a field name, field offset, field type, and a field length. By providing the expression parser with a DODA, you may include references to DODA field names in your expressions. See the sample code shown later in this section for an example of a DODA definition.
While a DODA is conveniently defined in your application using an array of DATOBJ structures, cndxparse() does not take a DODA in DATOBJ form, but instead accepts a record schema and a list of the field names from the DODA. In order to simplify converting your DODA into the required record schema and field name list, FairCom has written a utility function, ctparsedoda(). This function can be found in the sample file ctexpr.c.
ctparsedoda() is declared as follows:
COUNT ctparsedoda(pDATOBJ doda, UCOUNT numfld, ppTEXT ppschema,
ppTEXT ppnames)
Evaluating Expressions
Having produced an expression tree, you are ready to evaluate the expression using the expression analyzer. To evaluate your expression, call cndxeval(), which is declared as follows:
COUNT cndxeval(PTREE Tree, pVOID Recptr, pConvMap Schema)
- Tree is an expression tree returned by cndxparse().
- Recptr points to a buffer containing data you wish to evaluate with your expression.
- Schema is the record schema ctparsedoda() produced from your DODA definition. The record schema is used to associate data in Recptr with field names specified in your expression.
Note: Before you call cndxeval() the first time, you must ensure that a run-time stack has been allocated for the expression analyzer.
To summarize, evaluating your expression involves three steps:
- Allocate a run-time stack for the expression analyzer (first time only).
- Set up a buffer containing the field data used in the expression.
- Evaluate the expression.
If you wish, you can repeat steps 2) and 3) multiple times. Sample code to perform these steps is shown below. It is assumed the Get_Buffer() routine allocates a record buffer and initializes it with data conforming to the field definitions specified in the DODA.
Expression Evaluation Example
COUNT retcidx; /* Result of expression evaluation. */
pTEXT recbuf; /* Record buffer. */
/* Allocate a run-time stack for the expression analyzer (first time only). */
if (!ctcidxStk) {
ctcidxStk = (pVOID) getcndxmem(CNDX_MAX_STACK * ctSIZE(PLEAF));
if (!ctcidxStk) {
printf("Unable to allocate memory for run-time stack.\n");
ctrt_exit(1);
}
}
/* Set up a buffer containing the field data used in the expression. */
Get_Buffer(&recbuf);
/* Evaluate the expression. */
retcidx = cndxeval(ptree, recbuf, (pConvMap)schema);
if (retcidx<0)
printf("The expression cannot be evaluated for this record
- error %d.\n", uerr_cod);
else if (retcidx)
printf("The expression evaluates to TRUE for this record.\n");
else
printf("The expression evaluates to FALSE for this record.\n");
Remember to always free any memory used by your record schema, field name list, and expression tree when you are finished with them. Use the following FairCom DB functions to do so:
- mbfree(schema)
- mbfree(names)
- cndxfree(ptree)
Variable-length Records with Conditional Expressions
When using data filters or conditional indexes with variable length data files, a record retrieval that does not bring back the entire record will return a CVAL_ERR error (598), which indicates the expression could not be evaluated.
There are two types of retrievals that result in less than the entire record being read:
- Calling the fixed-length versions of the ISAM routines such as FirstRecord() or NextRecord() instead of FirstVRecord() or NextVRecord(). The fixed-length calls cannot be used to read variable-length records with a data filter or conditional index.
- Calling the variable-length versions of the ISAM routines with a buffer length insufficient to hold the entire record.
When an ISAM call fails (with CVAL_ERR (598) or some other error), the current ISAM position is NOT updated. Therefore the following pseudo-code sequence will NOT work because the FirstRecord() did not establish the failing record as the current ISAM position, and the GETVLEN() call would reference the record at the current ISAM position before the FirstRecord() call:
SETFLTR(datno,...);
if (FRSREC(...) == CVAL_ERR) {
vlen= GETVLEN(datno);
rc= FRSVREC(...,&vlen);
}
Using the variable-length versions of the ISAM routines provides a workable approach. The following pseudo-code works, with one proviso - the subsequent calls to the ISAM routine can also fail with a CVAL_ERR (598) because they may have skipped forward to an even larger record:
SETFLTR(datno,...);
oldlen = vlen;
if (FRSVREC(...,bufr,&vlen) == CVAL_ERR && oldlen < vlen) {
free(bufr);
oldlen = vlen;
bufr = calloc(vlen);
rc = FRSVREC(...,bufr,&vlen);}
The second call to FirstVRecord() could also return the CVAL_ERR (598) because while the record that originally caused the CVAL_ERR (598) can now be read completely, if it failed the filter, the next records will be read automatically until a record is found that passes the filter; but these subsequent reads can also encounter a record that is bigger than the new buffer size.
The following pseudo-code loop should work with any of the variable length versions of the ISAM calls:
SETFLTR(datno,...);
oldlen= vlen;
while CVAL_ERR == (xyzVREC(...,bufr,&vlen) && oldlen < vlen) {
free(bufr);
oldlen = vlen;
bufr = calloc(vlen);
}
if (isam_err)
then problem or no record found
else
success
If one knows ahead of time that there is a maximum record length for the file, then simply using a buffer of this known maximum size eliminates the need to loop over the CVAL_ERR (598) caused by an insufficient buffer size.
Conditional Expression Callback Function
FairCom DB provides a developer-defined callback function to perform additional advanced custom data filtering and conditional index evaluation. Instead of calling c-tree’s internal expression evaluator to analyze criteria, a user-defined function is called, allowing advanced filtering and control with application specific code.
Callback Functions
The module ctclbk.c contains the routines declared below. ctfiltercb() is called when a user-defined filter must be evaluated. Whenever a file open retrieves stored conditional index callback expressions, when a conditional index callback expression is created, or when a SetDataFilter() callback expression is created, the ctfiltercb_init() routine is called with a pointer to the callback expression. Whenever a file is closed, or a SetDataFilter() is cleared, the ctfiltercb_uninit() routine is called for each callback expression.
NINT ctfiltercb_init(pTEXT Clbk pinHan)
NINT ctfiltercb(pTEXT Clbk, pVOID Recptr, pConvMap Schema,
VRLEN fixlen, VRLEN datlen pinHan)
NINT ctfiltercb_uninit(pTEXT Clbk pinHan)
Parameter |
Description |
|---|---|
pTEXT Clbk |
Pointer to a NULL terminated ASCII string beginning with the ctCNDXclbkCHR (@) character. Presumably, the string starting in the 2nd position (i.e., Cblk + 1) points to a callback function designator. |
pVOID Recptr |
Pointer to a record image |
pConvMap Schema |
Pointer to a record schema |
VRLEN fixlen |
Fixed length of the record image |
VRLEN datlen |
Full length of the record image |
pinHan |
A macro the converts to ctWNGV for standalone or client libraries defining ctNOGLOBALS or lctgv for Server or Bound Server code. |
ctfiltercb_init() is expected to return zero (0) on error and non-zero on initialization, though at this point the error return is ignored. ctfiltercb_init() should set a state variable so the ctfiltercb_uninit() knows whether the init was called for this particular callback filter.
ctfiltercb_uninit() is expected to return zero (0) on error and non-zero on uninitialization, though at this point the error return is ignored. ctfiltercb_uninit() should check a state variable to determine whether or not ctfiltercb_init() was called for this particular callback filter.
Stub functions with simple debug print statements are part of the base distribution. A developer taking advantage of expression callback routines must adapt these callback functions to their particular requirements.
Callback Expression String
As described above, Clbk points to an ASCII string beginning with the "at" sign ‘@’. The string following ‘@’ is completely arbitrary, and is interpreted to determine what type of callback routine is intended, assuming more than one type of callback is required by the application. A simple scheme would use callback strings with a unique character in the second position (following ‘@’). This permits a simple switch statement routing callbacks to desired code, as shown in the following pseudo-code:
/*
** my callback strings are: @CustomerNumber
** @ZeroBalance
** @TotalFunds
*/
switch (*(Clbk + 1)) {
case 'C':
do the Customer Number check
break;
case 'Z':
do the Zero Balance check
break;
case 'T':
do the Total Funds check
break;
default:
set the return code to -CVAL_ERR to indicate the
filter could not be evaluated
break;
}
In this example scheme, the only significant portion of the callback designator is the first character after the @.
Record Batches
There are situations where a record fetch, a record insert, or a record delete operation has to be applied to a group of related records. For example, if an invoice record is being displayed, it may be necessary to retrieve all invoice items for that invoice as well. The following code fragment shows a typical FairCom DB API C function to extract all items of an invoice, given the invoice number.
FairCom DB API C API Example
void GetInvoiceItems(CTHANDLE hRecord, NINT Invoice)
{
NINT count = 0;
TEXT target[32];
VRLEN len = sizeof(target);
/* find the first record that match the Invoice */
ctdbClearRecord(hRecord);
ctdbSetFieldAsSigned(hRecord, 0, Invoice);
if (ctdbFindRecord(hRecord, CTFIND_GE) == CTDBRET_OK)
{
LONG val;
/* make sure all record invoice numbers match */
ctdbGetFieldAsSigned(hRecord, 0, &val);
while (val == (LONG)Invoice)
{
val = -1;
count++;
if (ctdbNextRecord(hRecord) == CTDBRET)OK)
ctdbGetFieldAsSigned(hRecord, 0, &val);
}
}
printf("%d records found\n", count);
}
The code fragment above shows how easy it is to use FairCom DB API functionality to retrieve a number of related records from a table. A close inspection of the code reveals that while it is simple to implement, it does have a hidden issue that may affect the overall performance of the system, specially when running in client/server mode: for every record that is fetched, the client must place a request to the server, which must travel the network, the c-tree Server must interpret the client request, perform the record retrieval operation and return the record to the client, again traveling across the network.
It is obvious the operation described above would be more efficient if we could, in just one call, request the c-tree Server to perform a given operation on a group of related records, and return all the resulting records, or as many records as possible, in one client request.
The ability of perform record retrievals, insert or delete operations on a group of related records is implemented under FairCom DB API as record batch operations. The same code fragment above implemented with FairCom DB API C API batch record facility would look as follows:
FairCom DB API C API Batch Example
void GetInvoiceItems(CTHANDLE hRecord, NINT Invoice)
{
NINT count = 0;
/* set the partial target key */
ctdbClearRecord(hRecord);
ctdbSetFieldAsSigned(hRecord, 0, Invoice);
/* set the batch operation */
if (ctdbSetBatch(hRecord, CTBATCH_GET, sizeof(Invoice), 0) == CTDBRET_OK)
{
/* retrieve records */
while (ctdbNextBatch(hRecord) == CTDBRET_OK)
count++;
/* terminate batch operations */
ctdbEndBatch(hRecord);
}
printf("%d records found\n", count);
}
The code fragment above is even simpler than the initial example, and it also performs far fewer c-tree Server calls to perform the operations. FairCom DB API batch record functionality is implemented on top of c-tree ISAM batch operations.
Batches with FairCom DB API
The following types of batch operations may be performed:
Retrieve records by partial key
All records with keys matching a partial target key are loaded into an internally maintained buffer region. If there are more records than fit in the buffer, those that fit are loaded, and subsequent calls will retrieve the remaining records.
Retrieve records by index range
Retrieve all records that match an index range expression. All matched records are loaded into an internally maintained buffer region. If there are more records than fit in the buffer, those that fit are loaded, and a subsequent call will retrieve the remaining records.
Retrieve records by physical order
All records of a table are loaded by physical order into an internally maintained buffer region. If the selected records do not fit in the buffer, those that fit are loaded, and subsequent calls will retrieve the remaining records.
Insert a group of records
A group of new records are loaded into an internally maintained buffer region and this group of records are inserted into a table.
Delete a group of records
All records with a key matching a partial target key are deleted.
Batch Modes
You must specify one of the following Mandatory modes when calling the ctdbSetBatch() function or the CTRecord:SetBatch() method:
MODE |
Description |
|---|---|
CTBATCH_GET |
Retrieve a group of related records by partial key |
CTBATCH_RANGE |
Retrieve a group of related records based on an index range expression |
CTBATCH_PHYS |
Retrieve records from a table in physical order. The starting record for the batch retrieval may be specified. (A physical order batch read is slightly faster than going through an index as it has fewer reads to do.) |
CTBATCH_DEL |
Delete a group of related records by partial key |
CTBATCH_INS |
Insert a group of records |
The following modes are optional and can be OR-ed to the mandatory mode to specify other details on how the batch operation is to be performed.
Mode |
Description |
|---|---|
CTBATCH_GKEY |
Process records with a greater than or equal key match with the target key. When this mode is specified, the number of matched records is not readily available. ctdbBatchLocked() and CTRecord::BatchLocked returns a value one greater than ctdbBatchLoaded() to indicate there may be more records to process.This mode is applicable only with CTBATCH_GET and CTBATCH_DEL modes and can not be used with CTBATCH_LKEY. |
CTBATCH_LKEY |
Process records that have a less than or equal key match with the target key.This mode is applicable only with CTBATCH_GET and CTBATCH_DEL modes and can not be used with CTBATCH_GKEY. |
CTBATCH_VERIFY |
Verify that the keys in the index match the values in the key fields of the record. |
CTBATCH_LOCK_KEEP |
Keep all records locked after ...EndBatch() is called. Without this mode, all records locks are released when ...EndBatch() is called. This option is only in effect when used with CTBATCH_LOCK_READ or CTBATCH_LOCK_WRITE. |
CTBATCH_LOCK_READ |
Place a read lock on each record that matches the partial key. |
CTBATCH_LOCK_WRITE |
Place a write lock on each record that matches the partial key. |
CTBATCH_LOCK_BLOCK |
Convert a CTBATCH_LOCK_READ or CTBATCH_LOCK_WRITE to blocking read and blocking write locks, respectively. |
CTBATCH_LOCK_ONE |
Implement an alternative locking strategy: only locks the record during the record read; original locking strategy keeps locks on during entire batch processing. |
CTBATCH_COMPLETE |
...SetBatch() returns a success code only if all matching records are successfully locked. You must specify either CTBATCH_LOCK_READ or CTBATCH_LOCK_WRITE. |
Starting a new batch operation
The way a new batch operation is started will depend on the type of batch operation you are performing:
- Retrieving records by partial key
- Retrieving records by index range
- Retrieving records by physical order
- Deleting a group of records
- Inserting a group of records
Each of these situations is discussed below.
Retrieving records by partial key
All records with key matching a partial target key are loaded into a buffer region maintained internally by FairCom DB API. If the selected records do not fit in the buffer, those that fit are loaded, and subsequent calls will retrieve the remaining records.
The following steps must be taken to perform a batch retrieval operation based on a partial key:
- Clear a record buffer by calling the CTRecord:Clear() method.
- Use CTRecord::SetFieldAs...() methods to set the fields that form the partial target key that will be used to select a group of records.
- Call the CTRecord::SetBatch() method, with the CTBATCH_GET mode, to start a new record retrieval batch operation.
- Call the CTRecord::NextBatch() method repeatedly until all related records are retrieved. CTRecord::NextBatch() returns false to indicate no more records are available.
- When you are done with the batch records, call the CTRecord::EndBatch() method to terminate the batch operation. Please note that another batch operation can only start after the current batch operation is terminated.
To start a new batch operation to retrieve records, you must first establish the partial target key that will identify the group of related records. This is accomplished by clearing a record buffer and setting the fields that form the partial target key. For example, if an invoice item table has one index with segments based on invoice number and item number fields, the following partial code will create a partial target key that will select all invoice item records associated with a particular invoice number:
Example
// set the partial target key
hRecord.Clear();
hRecord.SetFieldAsSigned("Invoice", Invoice);
After preparing the partial target key, a new batch operation is started by calling the CTRecord::SetBatch() method. Continuing from the example above, a new batch operation is started by performing the following call:
Example
try
{
// set the batch operation
hRecord.SetBatch(CTBATCH_GET, sizeof(Invoice), 0);
// retrieve and display all records
while (hRecord.NextBatch())
PrintRecord(hRecord);
// terminate batch operations
hRecord.EndBatch();
}
catch (CTException &err)
{
printf("Batch operation failed with error %d\n", err.GetErrorCode());
}
CTRecord::SetBatch() takes as first argument a valid record handle. The second argument is the batch mode that will direct how the batch operation will be performed. The chapters below describe the available batch modes in detail.
You must provide the length of the SetBatch()’s target key in bytes. The length of the target key will indicate how many bytes of the target key should be used to create the partial target key. The last parameter is the size of the buffer used internally by FairCom DB API to handle batch operations. A zero value for the last parameter is an indication that the default value size should be used. The default buffer size is calculated as the size of the fixed portion of the record multiplied by 128.
Retrieving records by index range
All records that match an index range expression are loaded into a buffer region maintained internally by FairCom DB API. If the selected records do not fit in the buffer, those that fit are loaded, and subsequent calls will retrieve the remaining records.
The following steps must be taken to perform an index range batch retrieval of records:
- Establish an index range by calling the CTRecord::RangeOn() method.
- Call the CTRecord::SetBatch() method with the CTBATCH_RANGE mode to start a new record retrieval batch operation.
- Call the CTRecord::NextBatch() method repeatedly until all related records are retrieved. CTRecord::NextBatch() returns false to indicate no more records are available
- When you are done with the batch records, call the CTRecord::EndBatch() method to terminate the batch operation.
- Call the CTRecord::RangeOff() method to terminate index range operations.
To start a new batch operation to retrieve records, you must first establish the index range operation that will identify the group of related records.
Example:
try
{
// build target key to be used in index range
TEXT lRange[32];
VRLEN len = (VRLEN)sizeof(lRange);
NINT op = CTIX_EQ;
hRecord.Clear();
hRecord.SetFieldAsSigned("Invoice", Invoice);
hRecord.BuildTargetKey(CTFIND_EQ, (pVOID)lRange, &len);
// set the index range based on the target key
hRecord.RangeOn(1, (pVOID)lRange, NULL, CTIX_EQ&op);
}
catch (CTException &err)
{
printf("Index range operation failed with error %d\n", err.GetErrorCode());
}
After setting the index range operation, you may start a new batch operation by calling the CTRecord::SetBatch() method.
Example:
try
{
// set the batch operation
hRecord.SetBatch(CTBATCH_RANGE, 0, 0);
// retrieve and display all records
while (hRecord.NextBatch())
PrintRecord(hRecord);
// terminate batch operations
hRecord.EndBatch();
// terminate the index range
hRecord.RangeOff();
}
catch (CTException &err)
{
printf("Batch operation failed with error %d\n", err.GetErrorCode());
}
Notice when retrieving records by index range SetBatch()’s targetLen parameter is set to zero. The last parameter is the size of the buffer used internally by FairCom DB API to handle batch operations. A zero value for the last parameter is an indication that the default value size should be used. The default buffer size is calculated as the size of the fixed portion of the record multiplied by 128.
Retrieving records by physical order
All records of a table are loaded by physical order into a buffer region maintained internally by FairCom DB API. If the selected records do not fit in the buffer, those that fit are loaded, and subsequent calls will retrieve the remaining records.
The following steps must be taken to perform a physical order batch retrieval of records:
- Call the CTRecord:::SetBatch() method with CTBATCH_PHYS mode to start a new record retrieval batch operation.
- Continue to call the CTRecord::NextBatch() method repeatedly until all related records are retrieved. CTRecord::NextBatch() returns false to indicate no more records are available.
- When you are done with the batch records, call the CTRecord::EndBatch() method to terminate the batch operation.
Example
try
{
// set the batch operation */
hRecord.SetBatch(CTBATCH_PHYS, 0, 0);
// retrieve and display all records
while (hRecord.NextBatch())
PrintRecord(hRecord);
// terminate batch operations
hRecord.EndBatch();
}
catch (CTException &err)
{
printf("Batch operation failed with error %d\n", err.GetErrorCode());
}
Notice when retrieving records by index range SetBatch()’s targetLen parameter is set to zero since no partial key is need for this operation. The last parameter is the size of the buffer used internally by FairCom DB API code to handle batch operations. A zero value for this parameter is an indication that the default value size should be used. The default buffer size is calculated as the size of the fixed portion of the record multiplied by 128.
Deleting a group of records
If the intended batch operation is to delete a group of selected records, you need to initially set the partial target key to select the group of related records and then start the batch operation to delete the selected records.
Even if no records are retrieved with the delete operation, CTRecord::EndBatch() must be called to terminate the current batch operation.
The following steps must be taken to perform a batch delete record operation:
- Clear a record buffer by calling the CTRecord::Clear() method.
- Use the CTRecord::SetFieldAs...() methods to set the fields that form the partial target key that will be used to select a group of records.
- Call the CTRecord::SetBatch() method with the CTBATCH_DEL mode to delete a group of related records.
- Call the CTRecord::EndBatch() method to terminate the delete record batch operation.
Example
try
{
// set the partial target key
hRecord.Clearh();
hRecord.SetFieldAsSigned("Invoice", Invoice);
// set the batch operation
hRecord.SetBatch(CTBATCH_DEL, sizeof(Invoice), 0);
// end the batch operation
hRecord.EndBatch();
}
catch (CTException &err)
{
printf("Batch operation failed with error %d\n", err.GetErrorCode());
}
You must provide the length of SetBatch()’s Target key in bytes. The length of the target key indicates how many bytes of the target key should be used to create the partial target key. The last parameter is the size of the buffer used internally by FairCom DB API to handle batch operations. A zero value for the last parameter indicates the default value size should be used. The default buffer size is calculated as the size of the fixed portion of the record multiplied by 128.
Inserting a group of records
A group of new records are loaded into a buffer region maintained internally by FairCom DB API and this group of records are inserted into a table.
When the batch buffer fills up, the group of records stored in the batch buffer are inserted into the table. If CTRecord::EndBatch() is called and the batch buffer still contains records, a new insert record operation is performed for the remaining records before the batch operation is terminated.
For transaction controlled files, the batch insertion operation is treated as one all or nothing operation. If no explicit transaction is started, each insertion of records with will start and end its own transaction. Even if an explicit transaction is started, each insertion operation is treated independently through safe points.
Currently, all records insertion operations do not perform any conversion of record images, key values and record position for heterogeneous client/server implementations.
The following steps must be taken to perform a batch insert record operation:
- Call the CTRecord::SetBatch() method, with CTBATCH_INS mode, to insert a group of records.
- For each record to be inserted perform the following operations:
- Call the CTRecord::Clear() method to clear a record buffer.
- For each field in the record call one of the CTRecord::SetFieldAs...() methods to set the field data.
- Call the CTRecord::InsertBatch() method to insert the record into the batch buffer.
- Call the CTRecord::EndBatch() method to indicate no more records will be inserted.
Example
try
{
// set the batch operation
hRecord.SetBatch(CTBATCH_INS, 0, 0);
// prepare the first record
hRecord.Clear();
hRecord.SetFieldAsSigned("Invoice", Invoice); // invoice number
hRecord.SetFieldAsSigned("ItemNbr", 1); // invoice item number
hRecord.SetFieldAsSigned("Quantity", 100); // item quantity
hRecord.SetFieldAsSigned("ItemCode", 1001); // item code
hRecord.InsertBatch(); // insert record in batch
// prepare the second record
hRecord.Clear();
hRecord.SetFieldAsSigned("Invoice", Invoice); // invoice number
hRecord.SetFieldAsSigned("ItemNbr", 2); // invoice item number
hRecord.SetFieldAsSigned("Quantity", 200); // item quantity
hRecord.SetFieldAsSigned("ItemCode", 1002); // item code
hRecord.InsertBatch(); // insert record in batch
// terminate the batch operation
hRecord.EndBatch();
}
catch (CTException &err)
{
printf("Batch operation failed with error %d\n", err.GetErrorCode());
}
Notice when inserting a group of records SetBatch()’s targetLen parameter is set to zero as no partial key is needed for this operation. The last parameter is the size of the buffer used internally by FairCom DB API to handle batch operations. A zero value for this parameter indicates the default value size should be used. The default buffer size is calculated as the size of the fixed portion of the record multiplied by 128.
Retrieving records
If the mode of the batch operation is one of CTBATCH_GET, CTBATCH_RANGE or CTBATCH_PHYS then it may be necessary to retrieve all records that match the batch criteria. The records are retrieved by calling the CTRecord::NextBatch() method.
CTRecordNextBatch() retrieves record data from the batch buffer maintained by FairCom DB API’s record handle. After a successful call to CTRecordNextBatch() the field data can be retrieved by calling the appropriate CTRecord::GetFieldAs...() functions.
Example
try
{
// retrieve records
while (CTRecord::NextBatch())
{
CTString invoice = hRecord.GetFieldAsString("Invoice");
CTString item = hRecord.GetFieldAsString("ItemNbr");
printf("%-11s %s\n", invoice.c_str(), item.c_str());
}
catch (CTException &err)
{
printf("Batch operation failed with error %d\n", err.GetErrorCode());
}
Terminating a batch operation
A batch operation must be terminated by calling the CTRecord::EndBatch() method. Once a batch operation is started, by calling the CTRecord::SetBatch() method no other batch operation is allowed to start until the current batch operation is terminated.
Example
try
{
// set the partial target key
hRecord.Clear();
hRecord.SetFieldAsSigned("Invoice", Invoice);
// set the batch operation
hRecord.SetBatch(CTBATCH_DEL, sizeof(Invoice), 0);
// end the batch operation
hRecord.EndBatch();
}
catch (CTException &err)
{
printf("Batch operation failed with error %d\n", err.GetErrorCode());
}
When performing batch retrieval operations, you may cancel the batch operation before retrieving all the records by calling the CTRecord::EndBatch() method.
If the batch operation is a CTBATCH_RANGE then you must also call the CTRecord::RangeOff() method to terminate the index range used for the batch operation.
Retrieving batch properties
Once a batch operation is started, the following batch properties can be retrieved by calling the appropriate functions or methods.
FairCom DB API C++ API Methods |
Batch Properties Returned |
|---|---|
CTRecord::BatchTotal |
Retrieves the total number of records matching the batch criteria. |
CTRecord::BatchLocked |
Retrieves the number of records locked by a batch operation. |
CTRecord::BatchLoaded |
Retrieves the number of records loaded in a batch buffer maintained by the FairCom DB API record handle. |
CTRecord::BatchMode |
Retrieves the batch mode set by the CTRecord::SetBatch() method. |
CTRecord::IsBatchActive |
Returns YES if a batch operation is active. |
Reading and writing field data to a record buffer
The FairCom DB API CTRecord class has the following methods to read field data from the record buffer:
Function name |
Explanation |
|---|---|
GetFieldAsBool |
Read field data as Boolean value |
GetFieldAsSigned |
Read field data as signed integer value |
GetFieldAsUnsigned |
Read field data as unsigned integer value |
GetFieldAsDate |
Read field data as date value |
GetFieldAsTime |
Read field data as time value |
GetFieldAsMoney |
Read field data as money value |
GetFieldAsFloat |
Read field data as double value |
GetFieldAsDateTime |
Read field data as time stamp value |
GetFieldAsString |
Read field data as string value |
GetFieldAsBinary |
Read field data as binary data |
GetFieldAsBlob |
Read field data as blob |
GetFieldAsBigint |
Read field data as signed 64 bit integer |
GetFieldAsCurrency |
Read field data as currency value |
GetFieldAsNumber |
Read field data as number (BCD) value |
// display all field data on screen
void DisplayData(CTTable &ATable, CTRecord &ARecord)
{
NINT count = ATable.GetFieldCount();
NINT i;
CTString str;
for (i = 0; i < count; i++)
{
ARecord.GetFieldAsString(i, str);
printf("Field %d: %s\n", i, str.c_str());
}
}
The following functions should be used to write fields into the data record buffer:
Function |
Explanation |
|---|---|
SetFieldAsBool |
update field data as Boolean value |
SetFieldAsSigned |
update field data as signed integer value |
SetFieldAsUnsigned |
update field data as unsigned integer value |
SetFieldAsDate |
update field data as date value |
SetFieldAsTime |
update field data as time value |
SetFieldAsMoney |
update field data as money value |
SetFieldAsFloat |
update field data as double value |
SetFieldAsDateTime |
update field data as time stamp value |
SetFieldAsString |
update field data as string value |
SetFieldAsBinary |
update field data as binary data |
SetFieldAsBlob |
update field data as blob |
SetFieldAsBigint |
update field data as signed 64 bit integer |
SetFieldAsCurrency |
update field data as currency value |
SetFieldAsNumber |
Read field data as number (BCD) value |
// add new record
void AddRecord(CTRecord &ARecord, const CTString& name, const CTString& address, const CTString& phone)
{
// clear the record buffer
ARecord.Clear();
// populate the record buffer with field data
ARecord.SetFieldAsString(0, name);
ARecord.SetFieldAsString(1, address);
ARecord.SetFieldAsString(2, phone);
// add the new record
ARecord.Write();
}
The SetFieldAs...() methods will also clear the null bit flag for the updated field.
When you invoke one of the GetFieldAs...() or SetFieldAs...() methods, you pass the field number or field name and the data you want to read from or write to the data record buffer.
If the type of the field you are trying to read from, or write to, is different to the type of the data specified by the GetFieldAs...() or SetFieldAs...() methods, the record manager will automatically convert the field data type to match the data type of the parameter of passed by one of the GetFieldAs...() or SetFieldAs...() method.
For example, if you are writing a report generator application that displays the fields of a record on screen, you can read all fields in the record buffer with GetFieldAsString() and the record manager will convert the different field types to string. Boolean field type is converted as "True" or "False", numeric values are converted to string and dates and times use the session wide default date and time formats.
Automatic data type conversion
Some automatic data type conversions may not be possible, and an error (CTDBRET_CANTCONVERT or CTDBRET_INVTYPE) will occur. This will happen, for instance, if the method CTRecord::SetFieldAsDate() is used to store data in a field whose type is CT_BOOL.
The table below presents the valid automatic conversions for the fields and the GetFieldAs...() or SetFieldAs...() methods.
Field Type*1 |
Function ...SetFieldAs... or ...GetFieldAs... |
|||||||||||||
|
Signed |
Unsigned |
Bigint |
Number |
Float |
Money |
Currency |
Date |
Time |
DateTime |
String |
Binary |
Bool |
Blob |
CT_BOOL |
X |
X |
X |
X |
|
|
X |
|
|
|
*2 |
X |
X |
X |
CT_TINYINT |
X |
X |
X |
X |
|
|
X |
|
|
|
X |
X |
X |
X |
CT_UTINYINT |
X |
X |
X |
|
|
|
|
|
|
|
X |
X |
X |
X |
CT_SMALLINT |
X |
X |
X |
X |
|
X |
X |
|
|
|
X |
X |
X |
X |
CT_USMALLINT |
X |
X |
X |
|
|
|
|
|
|
|
X |
X |
X |
X |
CT_INTEGER |
X |
X |
X |
X |
X |
X |
X |
X |
|
X |
X |
X |
X |
X |
CT_UINTEGER |
X |
X |
X |
|
|
|
|
X |
X |
|
X |
X |
X |
X |
CT_MONEY |
X |
X |
X |
X |
X |
X |
X |
X |
|
X |
X |
X |
X |
X |
CT_DATE |
X |
X |
X |
|
|
|
|
X |
|
|
X |
X |
X |
X |
CT_TIME |
X |
X |
X |
|
|
|
|
|
X |
|
X |
X |
X |
X |
CT_FLOAT |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_DOUBLE |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_TIMESTAMP |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_EFLOAT |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_BINARY |
X |
X |
X |
X |
X |
X |
X |
X |
X |
|
X |
X |
|
X |
CT_CHARS |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
|
X |
CT_FPSTRING |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
|
X |
CT_F2STRING |
X |
X |
X |
X |
X |
X |
X |
X |
X |
|
X |
X |
|
X |
CT_F4STRING |
X |
X |
X |
X |
X |
X |
X |
X |
X |
|
X |
X |
|
X |
CT_BIGINT |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_NUMBER |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_CURRENCY |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_PSTRING |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_VARBINARY |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_LVB |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
CT_VARCHAR or CT_LVC |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
*1 - These are the FairCom DB API field types. The old c-tree Plus field types are also valid and may be used. For new projects, the best approach is to use the new field type convention presented in this table, since this is the naming convention for the FairCom DB SQL field types.
*2 - The value being set must be equal to either “TRUE” or “FALSE” (case insensitive), otherwise the function will fail with CTDBRET_CANTCONVERT (4042).
For every row in the table above, there is a highlighted column. This column represents the natural function that should be used to update that particular field. As discussed, though, any other function (column) marked with an “X” can be used, and the appropriate conversions will be performed.
Null field support
The FairCom DB API record manager API implements the underlying support for null fields, but does not enforce it. If a table was created with null field support, the record manager will set or clear the null bit flag for fields, but there is no check to see if null fields are allowed or not.
When CTRecord::Clear() clears a record buffer, all fields are marked as null fields. As you update the field contents by calling SetFieldAs...() methods, the null field for the respective field is cleared indicating that the field contains data.
A specific field can be cleared, and its null field flag set, by calling CTRecord::ClearField() method. Call CTRecord::IsNullField() to check if a field null flag is set.
// if a field 0 is not null, clear the field
if (!ARecord.IsNullField(0))
{
ARecord.ClearField(0);
}
The FairCom DB API null flag controls the NOT NULL property of a column. Setting this to column has no effect on individual record null values: it is NOT enforced at the FairCom DB API layer. This attribute only applies to the FairCom DB SQL layer for constraint on values. It is useful to set this flag in c-tree data files before SQL import such that the property is maintained.
Field defined size
Call CTRecord::GetFieldSize() to retrieve the field defined size. The defined field size is the size of the field declared at the time the table was created.
Field actual length
Call CTRecord::GetFieldLength() to retrieve the field actual size. Variable length fields such as CT_VARCHAR and CT_VARBINARY actual size in a record may vary from the defined size.
// check if field 0 actual size is different from defined size
if (ARecord.GetFieldSize(0) != ARecord.GetFieldLength(0))
{
printf("Field 0 defined and actual sizes are different\n");
}
Field address in record buffer
The field address in a record buffer will also vary if the field is located in the variable portion of a record. Call ctdbGetFieldAddress() to retrieve the field address in the record buffer. Fields in the fixed portion of a record buffer will always return the same address.
CTRecord::GetFieldAddress() takes the field number as a parameter and returns a pointer to the address of the field in the record buffer. CTRecord::GetFieldAddress() returns NULL if an error occurs and the field address cannot be retrieved.
Field offset in record buffer
A field offset address in the record buffer can also be retrieved by calling CTRecord::GetFieldOffset(). The offsets of fields in the fixed portion of the record never change, while the offsets of fields in the variable portion of a record may change to accommodate the different sizes of variable length fields.
Check if field is fixed or variable length
Call CTRecord::IsVariableField() method to check if a field is variable length or not. CTRecord::IsVariableField() return YES if the field is variable length.
// check if field 0 is variable length
if (ARecord.IsVariableField(0))
{
printf("Field 0 is variable length\n");
}
else
{
printf("Field 0 is fixed length\n");
}
Using field names
Most CTRecord class methods that operate on fields have overloaded methods that will accept a field number or a field name as a parameter to indicate which field to operate on.
Clearing the record buffer
Call CTRecord::Clear() to clear the record buffer. The record buffer is cleared, the full bit flag for all fields are cleared, the new record flag is set to false, the edited record flag is cleared, and the record offset is set to zero.
// clear the record buffer
CTRecord ARecord(ATable);
try
{
ARecord.Clear();
}
catch (CTException &err)
{
printf("Record clear failed with error %d\n", err.GetErrorCode());
}
Clearing a field
A field can be cleared, and its null field flag set, by calling CTRecord::ClearField() method. Call CTRecord::IsNullField() to check if a field null flag is set.
// if a field 0 is not null, clear the field
if (!ARecord.IsNullField(0))
{
try
{
ARecord.ClearField(0);
}
catch (CTException &err)
{
printf("Clear field failed with error %d\n", err.GetErrorCode());
}
}
Adding new records
To add a new record to a table, you need to perform the following actions:
- Clear the record buffer
- Populate the fields by calling the methods CTRecord::SetFieldAs...()
- Add the record by calling CTRecord::Write()
// clear the record buffer
ARecord.Clear();
// populate the record buffer
ARecord.SetFieldAsSigned(0, 1000);
ARecord.SetFieldAsString(1, "Joe Smith");
// write the record
try
{
ARecord.Write();
}
catch (CTException &err)
{
printf("Add record failed with error %d\n", err.GetErrorCode());
}
A record is added to a table if the new record flag and edited flag are both set. CTRecord::Clear() sets the new record flag, while one of the CTRecord::SetFieldAs...() functions will set the edited record flag.
If you clear a record and, without populating the record buffer with data, a call to CTRecord::Write() returns no error code but no data record is written to disk, since the new record flag is set but the edited record flag is cleared.
If you wish to write a cleared or blank record to disk, you must clear the record buffer, set the edited record flag by calling ctdbSetEditedRecord() and then write the record by calling CTRecord::Write().
// clear the record buffer
ARecord.Clear();
// set the edited record flag
ARecord.SetEdited(YES);
// write the record
try
{
ARecord.Write();
}
catch (CTException &err)
{
printf("Write record failed with error %d\n", err.GetErrorCode());
}
Updating existing records
To update an existing record, you need to perform the following actions:
- Read the record from disk by calling one of the CTRecord::First(), CTRecord::Last(), CTRecord::Next(), CTRecord::Prev(), CTRecord::SeekRecord(), CTRecord::Find() or CTRecord::FindTarget() methods.
- Update one or more fields with one of the CTRecord::SetFieldAs...() methods.
- Write the record by calling CTRecord::Write().
// update the first record
ARecord.First();
// change the first field
ARecord.SetFieldAsString(0, "Joe Smith");
// write the record
try
{
ARecord.Write();
}
catch (CTException &err)
{
printf("Update record failed with error %d\n", err.GetErrorCode());
}
A record is updated when the new record flag is cleared and the edited record flag is set. The edited record flag is set when a record is read from disk and it indicates that this is an existing record.
If you read a record from disk, and without updating the record data, call CTRecord::Write(), no data record is written to disk, since the edited record flag is cleared.
A record update can be forced by setting the record edited flag with a call to CTRecord::SetEdited() method.
// force update of first record
ARecord.First();
// set the edited flag
ARecord.SetEdited(YES);
// write the record
try
{
ARecord.Write();
}
catch (CTException &err)
{
printf("Update record failed with error %d\n", err.GetErrorCode());
}
A record can be duplicated by reading it from disk, setting the edited record flag by calling CTRecord::SetEdited() and setting the new record flag by calling CTRecord::SetNew(), and calling CTRecord::Write() to write it to disk. Please note that a record can only be duplicated if the table has no indexes with unique keys.
// duplicate the first record
ARecord.First();
// set the edited flag
ARecord.SetEdited(YES);
// set the new record flag
ARecord.SetNew(YES);
// write the record
try
{
ARecord.Write();
}
catch (CTException &err)
{
printf("Write record failed with error %d\n", err.GetErrorCode());
}
Deleting records
To delete a record, you need to perform the following actions:
- Read the record from disk by calling one of the CTRecord::First(), CTRecord::Last(), CTRecord::Next(), CTRecord::Prev(), CTRecord::SeekRecord(), CTRecord::Find() or CTRecord::FindTarget() methods.
- Delete the record by calling CTRecord::Delete();
// delete the first record
ARecord.First();
// delete the record
try
{
ARecord.Delete();
}
catch (CTException &err)
{
printf("Delete record failed with error %d\n", err.GetErrorCode());
}
Record properties
New record flag
The new record flag indicates if a record has been cleared and not written to disk yet. The record manager uses this flag to decide if a record should be added or updated.
The new record flag is set when the record handle is allocated or when CTRecord::Clear() clears the record. The new record flag is cleared when a record is read from disk by calling one of the CTRecord::First(), CTRecord::Last(), CTRecord::Next(), CTRecord::Prev(), CTRecord::SeekRecord(), CTRecord::Find() or CTRecord::FindTarget() methods.
Use CTRecord::SetNew() to set the record new flag or CTRecord:IsNew() to check if a record is new or not.
Edited record flag
A record is only written to disk if the edited record flag is set. This flag is set when the record buffer is modified with a call to one of the CTRecord::SetFieldAs...() methods or by calling CTRecord::SetEdited() method. Use CTRecord::IsEdited() to check if a record has been edited or not.
Record offset
This property holds the current record offset of a record. If a record is cleared, the record offset property is zero. All records in a table, even the first record, will have a record offset value greater than zero.
You can retrieve the record offset value by calling CTRecord::GetRecordPos() method. You can set the record offset property, and load the record data at the offset, by calling CTRecord::SeekRecord().
Record count
Use CTRecord::GetRecordCount() to retrieve the total number of records in a table. This is a read only property.
// check if table is empty
if (ARecord.GetRecordCount() > 0)
{
printf("Table is not empty\n");
}
Record ROWID
Use CTRecord::GetRowid() method to retrieve the ROWID value for the current record. This is a read only property.
// retrieve the first record rowid
CTROWID rowid = ARecord.GetRowid();
Record Locking
A record lock can be acquired by performing the following actions:
- Read a record from disk by calling one of the CTRecord::First(), CTRecord::Last(), CTRecord::Next(), CTRecord::Prev(), CTRecord::SeekRecord(), CTRecord::Find(), or CTRecord::FindTarget() methods.
- Lock the record by calling CTRecord::LockRecord().
Session wide locks are better suited to implement data integrity because the records are locked as they are read from disk. Since a record lock can only be applied after the record is read, there is a window of opportunity for the record be modified or deleted by another thread before the record lock is acquired.
Release a record lock by calling CTRecord::UnlockRecord() or by calling CTRecord::LockRecord() with CTLOCK_FREE mode.
Check if a record is locked
Call CTRecord::GetRecordLock() to retrieve the current lock mode acquired for the current record. If the record is not locked, CTRecord::GetRecordLock() returns CTLOCK_FREE. If the current record is locked, CTRecord::GetRecordLock() returns the record lock mode.
// check if record is locked
if (ARecord.GetRecordLock() != CTLOCK_FREE)
{
printf("The record is locked\n");
}
The following record modes are returned by ctdbGetRecordLock():
Lock mode |
Explanation |
|---|---|
CTLOCK_FREE |
The record is not locked |
CTLOCK_READ |
The record has a read lock |
CTLOCK_WRITE |
The record has a write lock |
Demoting record locks
If a record has acquired a write lock, it is possible to change or demote the write lock to a read lock, if the record has not been updated. Use the CTRecord::LockRecord() method to demote a record with a write lock to a read lock.
// demote a write lock to a read lock
if (ARecord::GetRecordLock() == CTLOCK_WRITE)
{
try
{
ARecord.LockRecord(CTLOCK_READ);
}
catch (CTException &err)
{
printf("Demote lock failed with error %d\n", err.GetErrorCode());
}
}