Writing an XLA Event-Handler Application

There are general procedures for writing an XLA application that detects and reports changes to selected tables in a database.

This section describes these general procedures with the possible exception of Inspecting Column Data, the procedures described in this section are applicable to most XLA applications.

The following procedures are described:

The example code in this section is based on the xlaSimple sample application.

XLA functions mentioned here are documented in XLA Reference.

Tip:

In addition to files noted in TimesTen Include Files, an XLA application must include tt_xla.h.

Note:

To simplify the code examples, routine error checking code for each function call has been omitted. See XLA Error Handling.

Obtaining a Database Connection Handle

As with every ODBC application, an XLA application must initialize ODBC, obtain an environment handle (henv), and obtain a connection handle (hdbc) to communicate with the specific database.

This section shows how to obtain a connection handle.

Initialize the environment and connection handles:

SQLHENV henv = SQL_NULL_HENV;
SQLHDBC hdbc = SQL_NULL_HDBC;

Pass the address of henv to the SQLAllocEnv ODBC function to allocate an environment handle:

rc = SQLAllocEnv(&henv);

Pass the address of hdbc to the SQLAllocConnect ODBC function to allocate a connection handle for the database:

rc = SQLAllocConnect(henv, &hdbc);

Call the SQLDriverConnect ODBC function to connect to the database specified by the connection string (connStr), which in this example is passed from the command line:

static char connstr[CONN_STR_LEN];
...
rc = SQLDriverConnect(hdbc, NULL, (SQLCHAR*)connstr, SQL_NTS, NULL, 0,
                      NULL, SQL_DRIVER_COMPLETE);

Note:

After an ODBC connection handle is opened for use by an XLA application, the ODBC handle cannot be used for ODBC operations until the corresponding XLA handle is closed by calling ttXlaClose.

Call the SQLSetConnectOption ODBC function to turn autocommit off:

rc = SQLSetConnectOption(hdbc, SQL_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF);

Initializing XLA and Obtaining an XLA Handle

After initializing ODBC and obtaining an environment and connection handle, you can initialize XLA and obtain an XLA handle to access the transaction log.

See Obtaining a Database Connection Handle.

Create only one XLA handle per ODBC connection. If your application uses multiple XLA reader threads (each connected to its own XLA bookmark), create a separate XLA handle and ODBC connection for each thread.

This section describes how to initialize XLA. Before initializing XLA, initialize a bookmark. Then initialize an XLA handle as type ttXlaHandle_h:

unsigned char bookmarkName [32];
...
strcpy((char*)bookmarkName, "xlaSimple");
...
ttXlaHandle_h xla_handle = NULL;

Pass bookmarkName and the address of xla_handle to the ttXlaPersistOpen function to obtain an XLA handle:

rc = ttXlaPersistOpen(hdbc, bookmarkName, XLACREAT, &xla_handle);

The XLACREAT option is used to create a new non-replicated bookmark. Alternatively, use the XLAREPL option to create a replicated bookmark. In either case, the operation fails if the bookmark already exists.

To use a bookmark that already exists, call ttXlaPersistOpen with the XLAREUSE option, as shown in the following example.

#include <tt_errCode.h>      /* TimesTen Native Error codes */
...
    if ( native_error == 907 ) { /* tt_ErrKeyExists */
      rc = ttXlaPersistOpen(hdbc, bookmarkName, XLAREUSE, &xla_handle);
    ...
    }

If ttXlaPersistOpen is given invalid parameters, or the application was unable to allocate memory for the handle, the return code is SQL_INVALID_HANDLE. In this situation, ttXlaError cannot be used to detect this or any further errors.

If ttXlaPersistOpen fails but still creates a handle, the handle must be closed to prevent memory leaks.

Specifying Which Tables to Monitor for Updates

After initializing XLA and obtaining an XLA handle, you can specify which tables or materialized views you want to monitor for update events.

See Initializing XLA and Obtaining an XLA Handle.

You can determine which tables a bookmark is subscribed to by querying the SYS.XLASUBSCRIPTIONS table. You can also use SYS.XLASUBSCRIPTIONS to determine which bookmarks have subscribed to a specific table.

The ttXlaNextUpdate and ttXlaNextUpdateWait functions retrieve XLA records associated with DDL events. DDL XLA records are available to any XLA bookmark. DDL events include CREATAB, DROPTAB, CREAIND, DROPIND, CREATVIEW, DROPVIEW, CREATSEQ, DROPSEQ, CREATSYN, DROPSYN, ADDCOLS, DRPCOLS, and TRUNCATE transactions. See ttXlaUpdateDesc_t.

The ttXlaTableStatus function subscribes the current bookmark to updates to the specified table. Or it determines whether the current bookmark is already monitoring DML records associated with the table.

Call the ttXlaTableByName function to obtain both the system and user identifiers for a named table or materialized view. Then call the ttXlaTableStatus function to enable XLA to monitor changes to the table or materialized view.

Note:

LOB support in XLA is limited, as follows:

  • You can subscribe to tables containing LOB columns, but information about the LOB value itself is unavailable.

  • ttXlaGetColumnInfo returns information about LOB columns.

  • Columns containing LOBs are reported as empty (zero length) or null (if the value is actually NULL). In this way, you can tell the difference between a null column and a non-null column.

This example tracks changes to the MYDATA table.

#define TABLE_OWNER "APPUSER"
#define TABLE_NAME "MYDATA"
...
SQLUBIGINT SYSTEM_TABLE_ID = 0;
...
SQLUBIGINT userID;

rc = ttXlaTableByName(xla_handle, TABLE_OWNER, TABLE_NAME,
                      &SYSTEM_TABLE_ID, &userID);

When you have the table identifiers, you can use the ttXlaTableStatus function to enable XLA update tracking to detect changes to the MYDATA table. Setting the newstatus parameter to a nonzero value results in XLA tracking changes made to the specified table.

SQLINTEGER oldstatus;
SQLINTEGER newstatus = 1;
...
rc = ttXlaTableStatus(xla_handle, SYSTEM_TABLE_ID, 0,
                      &oldstatus, &newstatus);

The oldstatus parameter is output to indicate the status of the table at the time of the call.

At any time, you can use ttXlaTableStatus to return the current XLA status of a table by leaving newstatus null and returning only oldstatus. For example:

rc = ttXlaTableStatus(xla_handle, SYSTEM_TABLE_ID, 0,
                      &oldstatus, NULL);
...
if (oldstatus != 0)
     printf("XLA is currently tracking changes to table %s.%s\n",
             TABLE_OWNER, TABLE_NAME);
else
     printf("XLA is not tracking changes to table %s.%s\n",
             TABLE_OWNER, TABLE_NAME);

Retrieving Update Records From the Transaction Log

Once you have specified which tables to monitor for updates, you can call the ttXlaNextUpdate or ttXlaNextUpdateWait function to return a batch of records from the transaction log.

See ttXlaNextUpdate and ttXlaNextUpdateWait.

Only records for committed transactions are returned. They are returned in the order in which they were committed. You must periodically call the ttXlaAcknowledge function to acknowledge receipt of the transactions so that XLA can determine which records are no longer needed and can be purged from the transaction log. These functions impact the position of the application bookmark in the transaction log, as described in How Bookmarks Work. Also see ttLogHolds in Oracle TimesTen In-Memory Database Reference. That TimesTen built-in procedure returns information about transaction log holds.

Note:

The ttXlaAcknowledge function is an expensive operation and should be used only as necessary.

Each update record in a transaction returned by ttXlaNextUpdate begins with an update header described by the ttXlaUpdateDesc_t structure. This update header contains a flag indicating if the record is the first in the transaction (TT_UPDFIRST) or the last commit record (TT_UPDCOMMIT). The update header also identifies the table affected by the update. Following the update header are zero to two rows of data that describe the update made to that table in the database.

Figure 5-5 that follows shows a call to ttXlaNextUpdate that returns a transaction consisting of four update records from the transaction log. Receipt of the returned transaction is acknowledged by calling ttXlaAcknowledge, which resets the bookmark.

Note:

This example is simplified for clarity. An actual XLA application would likely read records for multiple transactions before calling ttXlaAcknowledge.

In this example, the xlaSimple application continues to monitor our table for updates until stopped by the user.

Before calling ttXlaNextUpdateWait, the example initializes a pointer to the buffer to hold the returned ttXlaUpdateDesc_t records (arry) and a variable to hold the actual number of returned records (records). Because the example calls ttXlaNextUpdateWait, it also specifies the number of seconds to wait (FETCH_WAIT_SECS) if no records are found in the transaction log buffer.

Next, call ttXlaNextUpdateWait, passing these values to obtain a batch of ttXlaUpdateDesc_t records in arry. Then process each record in arry by passing it to the HandleChange() function described in the example in Inspecting Record Headers and Locating Row Addresses. After all records are processed, call ttXlaAcknowledge to reset the bookmark position.

#define FETCH_WAIT_SECS 5
...
SQLINTEGER records;
ttXlaUpdateDesc_t** arry;
int j;

while (!StopRequested()) {

    /* Get a batch of update records */
    rc = ttXlaNextUpdateWait(xla_handle, &arry, 100,
                             &records, FETCH_WAIT_SECS);
    if (rc != SQL_SUCCESS {
      /* See XLA Error Handling */
    }

    /* Process the records */
    for(j=0; j < records; j++){
      ttXlaUpdateDesc_t* p;
      p = arry[j];
      HandleChange(p); /* Described in the next section */
    }

    /* After each batch, Acknowledge updates to reset bookmark.*/
    rc = ttXlaAcknowledge(xla_handle);
    if (rc != SQL_SUCCESS {
      /* See XLA Error Handling */
    }
} /* end while !StopRequested() */

The actual number of records returned by ttXlaNextUpdate or ttXlaNextUpdateWait, as indicated by the nreturned output parameter of those functions, may be less than the value of the maxrecords parameter. Figure 5-6 shows an example where maxrecords is 10, the transaction log contains transaction AT that is made up of seven records, and transaction BT that is made up of three records. In this case, both transactions are returned in the same batch and both maxrecords and nreturned values are 10. However, the next three transactions in the log are CT with 11 records, DT with two records, and ET with two records. Because the commit record for the DT transaction appears before the CT commit record, the next call to ttXlaNextUpdate returns the two records for the DT transaction and the value of nreturned is 2. In the next call to ttXlaNextUpdate, XLA detects that the total records for the CT transaction exceeds maxrecords, so it returns the records for this transaction in two batches. The first batch contains the first 10 records for CT (nreturned = 10). The second batch contains the last CT record and the two records for the ET transaction, assuming no commit record for a transaction following ET is detected within the next seven records.

See ttXlaNextUpdate and ttXlaNextUpdateWait.

Figure 5-6 Records Retrieved When maxrecords=10

Description of Figure 5-6 follows
Description of "Figure 5-6 Records Retrieved When maxrecords=10"

XLA reads records from either a memory buffer or transaction log files on the file system, as described in How XLA Reads Records From the Transaction Log. To minimize latency, records from the memory buffer are returned as soon as they are available, while records not in the buffer are returned only if the buffer is empty. This design enables XLA applications to see changes as soon as the changes are made and with minimal latency. The trade-off is that there may be times when fewer changes are returned than the number requested by the ttXlaNextUpdate or ttXlaNextUpdateWait maxrecords parameter.

Note:

For optimal throughput, XLA applications should make the "fetch" and "process record" procedures asynchronous. For example, you can create one thread to fetch and store the records and one or more other threads to process the stored records.

Inspecting Record Headers and Locating Row Addresses

Now that there is an array of update records where the type of operation each record represents is known, the returned row data can be inspected.

Each record returned by the ttXlaNextUpdate or ttXlaNextUpdateWait function begins with an ttXlaUpdateDesc_t header that describes the following:

  • The table on which the operation was performed

  • Whether the record is the first or last (commit) record in the transaction

  • The type of operation it represents

  • The length of the returned row data, if any

  • Which columns in the row were updated, if any

Figure 5-7 shows one of the update records in the transaction log.

Figure 5-7 Address of Row Data Returned in an XLA Update Record

Description of Figure 5-7 follows
Description of "Figure 5-7 Address of Row Data Returned in an XLA Update Record"

The ttXlaUpdateDesc_t header has a fixed length and, depending on the type of operation, is followed by zero to two rows (or tuples) from the database. You can locate the address of the first returned row by obtaining the address of the ttXlaUpdateDesc_t header and adding it to sizeof(ttXlaUpdateDesc_t):

tup1 = (void*) ((char*) ttXlaUpdateDesc_t + sizeof(ttXlaUpdateDesc_t));

This is shown in the example below.

The ttXlaUpdateDesc_t ->type field describes the type of SQL operation that generated the update. Transaction records of type UPDATETTUP describe UPDATE operations, so they return two rows to report the row data before and after the update. You can locate the address of the second returned row that holds the value after the update by adding the address of the first row in the record to its length:

if (ttXlaUpdateDesc_t->type == UPDATETUP) {
  tup2 = (void*) ((char*) tup1 + ttXlaUpdateDesc_t->tuple1);
}

This is also shown in the following example, which passes each record returned by the ttXlaNextUpdateWait function to a HandleChange() function, which determines whether the record is related to an INSERT, UPDATE, or CREATE VIEW operation. In this example, all other operations are ignored.

The HandleChange() function handles each type of SQL operation differently before calling the PrintColValues() function described in the example in Putting It All Together With a PrintColValues() Function.

void HandleChange(ttXlaUpdateDesc_t* xlaP)
{
  void*  tup1;
  void*  tup2;
 
  /* First confirm that the XLA update is for the table we care about. */
  if (xlaP->sysTableID != SYSTEM_TABLE_ID)
    return ;
 
  /* OK, it is for the table we are monitoring. */
 
  /* The last record in the ttXlaUpdateDesc_t record is the "tuple2"
   * field.  Immediately following this field is the first XLA record "row". */
 
  tup1 = (void*) ((char*) xlaP + sizeof(ttXlaUpdateDesc_t));
 
  switch(xlaP->type) {
 
  case INSERTTUP:
    printf("Inserted new row:\n");
    PrintColValues(tup1);
    break;
 
  case UPDATETUP:
 
    /* If this is an update ttXlaUpdateDesc_t, then following that is
     * the second XLA record "row".  
     */
 
    tup2 = (void*) ((char*) tup1 + xlaP->tuple1);
    printf("Updated row:\n");
    PrintColValues(tup1);
    printf("To:\n");
    PrintColValues(tup2);
    break;
 
  case DELETETUP:
    printf("Deleted row:\n");
    PrintColValues(tup1);
    break;
 
  default:
    /* Ignore any XLA records that are not for inserts/update/delete SQL ops. */
    break;
 
  } /* switch (xlaP->type) */
}

Inspecting Column Data

There are methods for inspecting column data.

Data Returned in an Update Record

Zero to two rows of data may be returned in an update record after the ttXlaUpdateDesc_t structure.

See ttXlaUpdateDesc_t and Inspecting Record Headers and Locating Row Addresses.

For each row, the first portion of the data is the fixed-length data, which is followed by any variable-length data, as shown in Figure 5-8.

Figure 5-8 Column Offsets in a Row Returned in an XLA Update Record

Description of Figure 5-8 follows
Description of "Figure 5-8 Column Offsets in a Row Returned in an XLA Update Record"

Obtaining Column Descriptions

To read the column values from the returned row, you must first know the offset of each column in that row.

The column offsets and other column metadata can be obtained for a particular table by calling the ttXlaGetColumnInfo function, which returns a separate ttXlaColDesc_t structure for each column in the table. You should call the ttXlaGetColumnInfo function as part of your initialization procedure. This call was omitted from the discussion in Initializing XLA and Obtaining an XLA Handle for simplicity.

When calling ttXlaGetColumnInfo, specify a colinfo parameter to create a pointer to a buffer to hold the list of returned ttXlaColDesc_t structures. Use the maxcols parameter to define the size of the buffer.

The sample code from the xlaSimple application below guesses the maximum number of returned columns (MAX_XLA_COLUMNS), which sets the size of the buffer xla_column_defs to hold the returned ttXlaColDesc_t structures. An alternative and more precise way to set the maxcols parameter would be to call the ttXlaGetTableInfo function and use the value returned in ttXlaColDesc_t ->columns.

#define MAX_XLA_COLUMNS 128
...
SQLINTEGER ncols;
...
ttXlaColDesc_t xla_column_defs[MAX_XLA_COLUMNS];
...
rc = ttXlaGetColumnInfo(xla_handle, SYSTEM_TABLE_ID, userID,
             xla_column_defs, MAX_XLA_COLUMNS, &ncols);
  if (rc != SQL_SUCCESS {
    /* See "XLA Error Handling" */
}

As shown in Figure 5-9, the ttXlaGetColumnInfo function produces the following output:

  • A list, xla_column_defs, of ttXlaColDesc_t structures into the buffer pointed to by the ttXlaGetColumnInfo colinfo parameter

  • An nreturned value, ncols, that holds the actual number of columns returned in the xla_column_defs buffer

Figure 5-9 ttXlaColDesc_t structures Returned by ttXlaGetColumnInfo

Description of Figure 5-9 follows
Description of "Figure 5-9 ttXlaColDesc_t structures Returned by ttXlaGetColumnInfo"

Each ttXlaColDesc_t structure returned by ttXlaGetColumnInfo has an offset value that describes the offset location of that column. How you use this offset value to read the column data depends on whether the column contains fixed-length data (such as CHAR, NCHAR, INTEGER, BINARY, DOUBLE, FLOAT, DATE, TIME, TIMESTAMP, and so on) or variable-length data (such as VARCHAR, NVARCHAR, or VARBINARY).

Reading Fixed-Length Column Data

For fixed-length column data, the address of a column is the offset value in the ttXlaColDesc_t structure, plus the address of the row.

See ttXlaColDesc_t.

Figure 5-10 Locating Fixed-Length Data in a Row

Description of Figure 5-10 follows
Description of "Figure 5-10 Locating Fixed-Length Data in a Row"

See the example in Putting It All Together With a PrintColValues() Function for a complete working example of computations such as those shown here.

The first column in the MYDATA table is of type CHAR. If you use the address of the tup1 row obtained earlier in the HandleChange() function (in the example in Inspecting Record Headers and Locating Row Addresses) and the offset from the first ttXlaColDesc_t structure returned by the ttXlaGetColumnInfo function (in the example in Obtaining Column Descriptions), you can obtain the value of the first column with computations such as the following:

char*  Column1;

Column1 = ((unsigned char*) tup1 + xla_column_defs[0].offset);

The third column in the MYDATA table is of type INTEGER, so you can use the offset from the third ttXlaColDesc_t structure to locate the value and recast it as an integer using computations such as the following. The data is guaranteed to be aligned properly.

int Column3;

Column3 = *((int*) ((unsigned char*) tup +
           xla_column_defs[2].offset));

The fourth column in the MYDATA table is of type NCHAR, so you can use the offset from the fourth ttXlaColDesc_t structure to locate the value and recast it as a SQLWCHAR type, with computations such as the following:

SQLWCHAR*  Column4;

Column4 = (SQLWCHAR*) ((unsigned char*) tup +
                      xla_column_defs[3].offset);

Unlike the column values obtained in the above examples, Column4 points to an array of two-byte Unicode characters. You must iterate through each element in this array to obtain the string, as shown for the SQL_WCHAR case in the example in Putting It All Together With a PrintColValues() Function.

Other fixed-length data types can be cast to their corresponding C types. Complex fixed-length data types, such as DATE, TIME, and DECIMAL values, are stored in an internal TimesTen format, but can be converted by applications to their corresponding ODBC C value using the XLA conversion functions, as described in Converting Complex Data Types.

Note:

Strings returned by XLA are not null-terminated. See Null-Terminating Returned Strings.

Reading NOT INLINE Variable-Length Column Data

For NOT INLINE variable-length data (VARCHAR, NVARCHAR, and VARBINARY), the data located at ttXlaColDesc_t ->offset is a four-byte offset value that points to the location of the data in the variable-length portion of the returned row.

By adding the offset address to the offset value, you can obtain the address of the column data in the variable-length portion of the row. The first eight bytes at this location is the length of the data, followed by the actual data. For variable-length data, the ttXlaColDesc_t ->size value is the maximum allowable column size. Figure 5-11 shows how to locate NOT INLINE variable-length data in a row.

Figure 5-11 Locating NOT INLINE Variable-Length Data in a Row

Description of Figure 5-11 follows
Description of "Figure 5-11 Locating NOT INLINE Variable-Length Data in a Row"

See the example in Putting It All Together With a PrintColValues() Function for a complete working example of computations such as those shown here.

Continuing with our example, the second column in the returned row (tup1) is of type VARCHAR. To locate the variable-length data in the row, first locate the value at the column's ttXlaColDesc_t ->offset in the fixed-length portion of the row, as shown in Figure 5-11 above. The value at this address is the four-byte offset of the data in the variable-length portion of the row (VarOffset). Next, obtain a pointer to the beginning of the variable-length column data (DataLength) by adding the VarOffset offset value to the address of VarOffset. The first eight bytes at the DataLength location is the length of the data. The next byte after DataLength is the beginning of the actual data (Column2).

void*  VarOffset; /* offset of data */
long*  DataLength; /* length of data */
char*  Column2; /* pointer to data */

VarOffset = (void*) ((unsigned char*) tup1 +
             xla_column_defs[1].offset);
/*
 * If column is out-of-line, pColVal points to an offset
 * else column is inline so pColVal points directly to the string length.
 */

if (xla_column_defs[1].flags & TT_COLOUTOFLINE)
   DataLength = (long*)((char*)VarOffset + *((int*)VarOffset));
else
   DataLength = (long*)VarOffset;
Column2 = (char*)(DataLength+1);

VARBINARY types are handled in a manner similar to VARCHAR types. If Column2 were an NVARCHAR type, you could initialize it as a SQLWCHAR, get the value as shown in the above VARCHAR case, then iterate through the Column2 array, as shown for the NCHAR value, CharBuf, in Putting It All Together With a PrintColValues() Function.

Note:

In the example, DataLength is type long, which is a 64-bit (eight-byte) type on UNIX-based 64-bit systems. On Windows 64-bit systems, where long is a four-byte type, the eight-byte type __int64 would be used instead.

Null-Terminating Returned Strings

Strings returned from record row data are not terminated with a null character. You can null-terminate a string by copying it into a buffer and adding a null character, '\0', after the last character in the string.

The procedures for null-terminating fixed-length and variable-length strings are slightly different. The examples that follow show the processes for null-terminating fixed-length strings, null-terminating variable-length strings of a known size, and null-terminating variable-length strings of an unknown size.

See the example inPutting It All Together With a PrintColValues() Function for a complete working example of computations such as those shown here.

To null-terminate the fixed-length CHAR(10) Column1 string returned in the example in "Reading Fixed-Length Column Data", establish a buffer large enough to hold the string plus null character. Next, obtain the size of the string from ttXlaColDesc_t ->size, copy the string into the buffer, and null-terminate the end of the string, using computations such as the following. You can now use the contents of the buffer. In this example, the string is printed:

char buffer[10+1];
int size;

size = xla_column_defs[0].size;
memcpy(buffer, Column1, size);
buffer[size] = '\0';

printf(" Row %s is %s\n", ((unsigned char*) xla_column_defs[0].colName), buffer);

Null-terminating a variable-length string is similar to the procedure for fixed-length strings, only the size of the string is the value located at the beginning of the variable-length data offset, as described in Reading NOT INLINE Variable-Length Column Data.

If the Column2 string obtained in the example in Reading NOT INLINE Variable-Length Column Data is a VARCHAR(32), establish a buffer large enough to hold the string plus null character. Use the value located at the DataLength offset to determine the size of the string, using computations such as the following:

char buffer[32+1];

memcpy(buffer, Column2, *DataLength);
buffer[*DataLength] = '\0';

printf(" Row %s is %s\n", ((unsigned char*) xla_column_defs[1].colName), buffer);

If you are writing general purpose code to read all data types, you cannot make any assumptions about the size of a returned string. For strings of an unknown size, statically allocate a buffer large enough to hold the majority of returned strings. If a returned string is larger than the buffer, dynamically allocate the correct size buffer, as shown in the example immediately below.

If the Column2 string obtained in the example in Reading NOT INLINE Variable-Length Column Data is of an unknown size, you might statically allocate a buffer large enough to hold a string of up to 10,000 characters. Then check that the DataLength value obtained at the beginning of the variable-length data offset is less than the size of the buffer. If the string is larger than the buffer, use malloc() to dynamically allocate the buffer to the correct size.

#define STACKBUFSIZE 10000
char VarStackBuf[STACKBUFSIZE];
char*  buffer;

buffer = (*DataLength+1 <= STACKBUFSIZE) ? VarStackBuf :
           malloc(*DataLength+1);

memcpy(buffer,Column2,*DataLength);
buffer[*DataLength] = '\0';

printf(" Row %s is %s\n", ((unsigned char*) xla_column_defs[1].colName), buffer);
if (buffer != VarStackBuf) /* buffer was allocated */
        free(buffer);

Converting Complex Data Types

There are methods to convert complex data types.

See the example in Putting It All Together With a PrintColValues() Function for a complete working example of computations such as those shown here.

Values for complex data types such as TT_DATE and TT_TIME are stored in an internal TimesTen format that can be converted into corresponding ODBC C types using XLA type conversion functions described in XLA Data Type Conversion Functions.

If you use the address of the tup1 row obtained earlier in the HandleChange() function (see the example in Inspecting Record Headers and Locating Row Addresses) and the offset from the fifth ttXlaColDesc_t structure returned by the ttXlaGetColumnInfo function (see the example in Obtaining Column Descriptions), you can locate a column value of type TIMESTAMP. Use the ttXlaTimeStampToODBCCType function to convert the column data from TimesTen format and store the converted time value in an ODBC TIMESTAMP_STRUCT. You could use code such as the following to print the values:

void*  Column5;
TIMESTAMP_STRUCT timestamp;

Column5 = (void*) ((unsigned char*) tup1 +
                  xla_column_defs[4].offset);

rc = ttXlaTimeStampToODBCCType(Column5, &timestamp);
if (rc != SQL_SUCCESS) {
  /* See XLA Error Handling */
}
printf(" %s: %04d-%02d-%02d %02d:%02d:%02d.%06d\n",
      ((unsigned char*) xla_column_defs[i].colName),
        timestamp.year,timestamp.month, timestamp.day,
        timestamp.hour,timestamp.minute,timestamp.second,
        timestamp.fraction);

If you use the address of the tup1 row obtained earlier in the HandleChange() function and the offset from the sixth ttXlaColDesc_t structure returned by the ttXlaGetColumnInfo function, you can locate a column value of type DECIMAL. Use the ttXlaDecimalToCString function to convert the column data from TimesTen decimal format to a string. You could use code such as the following to print the values.

char decimalData[50];

Column6 = (float*) ((unsigned char*) tup +
           xla_column_defs[5].offset);
precision = (short) (xla_column_defs[5].precision);
scale = (short) (xla_column_defs[5].scale);

rc = ttXlaDecimalToCString(Column6, (char*)&decimalData,
                           precision, scale);
if (rc != SQL_SUCCESS) {
  /* See XLA Error Handling */
}

printf(" %s: %s\n", ((unsigned char*) xla_column_defs[5].colName), decimalData);

Detecting Null Values

For nullable table columns, ttXlaColDesc_t ->nullOffset points to the column's null byte in the record. This field is 0 (zero) if the column is not nullable, or greater than 0 if the column can be null.

For nullable columns (ttXlaColDesc_t ->nullOffset > 0), to determine if the column is null, add the null offset to the address of ttXlaUpdate_t* and check the (unsigned char) byte there to see if it is 1 (NULL) or 0 (NOT NULL).

Check whether Column6 is null as follows:

if (xla_column_defs[5].nullOffset != 0) {
  if (*((unsigned char*) tup +
     xla_column_defs[5].nullOffset) == 1) {
         printf("Column6 is NULL\n");
  }
}

XLA Data Type Conversion Functions

This section lists XLA data type conversion functions that convert from internal TimesTen formats to ODBC C types.

These conversion functions can be used on row data in the ttXlaUpdateDesc_t types: UPDATETUP, INSERTTUP and DELETETUP.

Table 5-2 XLA Data Type Conversion Functions

Function Converts

ttXlaDateToODBCCType

Internal TT_DATE value to an ODBC C value

ttXlaTimeToODBCCType

Internal TT_TIME value to an ODBC C value

ttXlaTimeStampToODBCCType

Internal TT_TIMESTAMP value to an ODBC C value

ttXlaDecimalToCString

Internal TTXLA_DECIMAL_TT value to a string value

ttXlaDateToODBCCType

Internal TTXLA_DATE_TT value to an ODBC C value

ttXlaDecimalToCString

Internal TTXLA_DECIMAL_TT value to a character string

ttXlaNumberToBigInt

Internal TTXLA_NUMBER value to a TT_BIGINT value

ttXlaNumberToCString

Internal TTXLA_NUMBER value to a character string

ttXlaNumberToDouble

Internal TTXLA_NUMBER value to a long floating point number value

ttXlaNumberToInt

Internal TTXLA_NUMBER value to an integer

ttXlaNumberToSmallInt

Internal TTXLA_NUMBER value to a TT_SMALLINT value

ttXlaNumberToTinyInt

Internal TTXLA_NUMBER value to a TT_TINYINT value

ttXlaNumberToUInt

Internal TTXLA_NUMBER value to an unsigned integer

ttXlaOraDateToODBCTimeStamp

Internal TTXLA_DATE value to an ODBC timestamp

ttXlaOraTimeStampToODBCTimeStamp

Internal TTXLA_TIMESTAMP value to an ODBC timestamp

ttXlaTimeToODBCCType

Internal TTXLA_TIME value to an ODBC C value

ttXlaTimeStampToODBCCType

Internal TTXLA_TIMESTAMP_TT value to an ODBC C value

Putting It All Together With a PrintColValues() Function

There is a function that checks the ttXlaColDesc_t ->dataType of each column to locate columns with a data type of CHAR, NCHAR, INTEGER, TIMESTAMP, DECIMAL, and VARCHAR, then prints the values.

This is just one possible approach. Another option, for example, would be to check the ttXlaColDesc_t ->ColName values to locate specific columns by name.

The PrintColValues() function handles CHAR and VARCHAR strings up to 50 bytes in length. NCHAR characters must belong to the ASCII character set.

The function in this example first checks ttXlaColDesc_t ->nullOffset to see if the column is null. Next it checks the ttXlaColDesc_t ->dataType field to determine the data type for the column. For simple fixed-length data (CHAR, NCHAR, and INTEGER), it casts the value located at ttXlaColDesc_t ->offset to the appropriate C type. The complex data types, TIMESTAMP and DECIMAL, are converted from their TimesTen formats to ODBC C values using the ttXlaTimeStampToODBCCType and ttXlaDecimalToCString functions.

For variable-length data (VARCHAR), the function locates the data in the variable-length portion of the row, as described in XLA Error Handling.

void PrintColValues(void* tup)
{ 
 
  SQLRETURN rc ;
  SQLINTEGER native_error;
 
  void* pColVal;
  char buffer[50+1]; /* No strings over 50 bytes */
  int i;
 
  for (i = 0; i < ncols; i++)
  {
 
    if (xla_column_defs[i].nullOffset != 0) {  /* See if column is NULL */
      /* this means col could be NULL */
      if (*((unsigned char*) tup + xla_column_defs[i].nullOffset) == 1) {
        /* this means that value is SQL NULL */
        printf("  %s: NULL\n", 
               ((unsigned char*) xla_column_defs[i].colName));
        continue; /* Skip rest and re-loop */
      }
    }
 
    /* Fixed-length data types: */
    /* For INTEGER, recast as int */
 
    if (xla_column_defs[i].dataType == TTXLA_INTEGER) {
 
      printf("  %s: %d\n",
             ((unsigned char*) xla_column_defs[i].colName),
             *((int*) ((unsigned char*) tup + xla_column_defs[i].offset)));
    }

    /* For CHAR, just get value and null-terminate string */
 
    else if (   xla_column_defs[i].dataType == TTXLA_CHAR_TT
             || xla_column_defs[i].dataType == TTXLA_CHAR) {
 
      pColVal = (void*) ((unsigned char*) tup + xla_column_defs[i].offset);
 
      memcpy(buffer, pColVal, xla_column_defs[i].size);
      buffer[xla_column_defs[i].size] = '\0';
 
      printf("  %s: %s\n", ((unsigned char*) xla_column_defs[i].colName), buffer);
    }
 
    /* For NCHAR, recast as SQLWCHAR.
       NCHAR strings must be parsed one character at a time */
 
    else if (   xla_column_defs[i].dataType == TTXLA_NCHAR_TT  
             || xla_column_defs[i].dataType == TTXLA_NCHAR ) {
      SQLUINTEGER j;
      SQLWCHAR* CharBuf;
 
      CharBuf = (SQLWCHAR*) ((unsigned char*) tup + xla_column_defs[i].offset);

      printf("  %s: ", ((unsigned char*) xla_column_defs[i].colName));
 
      for (j = 0; j < xla_column_defs[i].size / 2; j++)
      { 
        printf("%c", CharBuf[j]);
      }
      printf("\n");
    }
    /* Variable-length data types:
       For VARCHAR, locate value at its variable-length offset and null-terminate.
       VARBINARY types are handled in a similar manner.
       For NVARCHARs, initialize 'var_data' as a SQLWCHAR, get the value as shown 
       below, then iterate through 'var_len' as shown for NCHAR above */
 
    else if (   xla_column_defs[i].dataType == TTXLA_VARCHAR
             || xla_column_defs[i].dataType == TTXLA_VARCHAR_TT) {
 
      long*  var_len;
      char* var_data;
      pColVal = (void*) ((unsigned char*) tup + xla_column_defs[i].offset);
      /*
       * If column is out-of-line, pColVal points to an offset
       * else column is inline so pColVal points directly to the string length.
       */
      if (xla_column_defs[i].flags & TT_COLOUTOFLINE)
        var_len = (long*)((char*)pColVal + *((int*)pColVal));
      else
        var_len = (long*)pColVal;
 
      var_data = (char*)(var_len+1);
 
      memcpy(buffer,var_data,*var_len);
      buffer[*var_len] = '\0';
 
      printf("  %s: %s\n", ((unsigned char*) xla_column_defs[i].colName), buffer);
    }
    /* Complex data types require conversion by the XLA conversion methods
       Read and convert a TimesTen TIMESTAMP value.
       DATE and TIME types are handled in a similar manner  */
 
    else if (   xla_column_defs[i].dataType == TTXLA_TIMESTAMP
             || xla_column_defs[i].dataType == TTXLA_TIMESTAMP_TT) {
 
      TIMESTAMP_STRUCT timestamp;
      char* convFunc;
 
      pColVal = (void*) ((unsigned char*) tup + xla_column_defs[i].offset);
 
      if (xla_column_defs[i].dataType == TTXLA_TIMESTAMP_TT) {
        rc = ttXlaTimeStampToODBCCType(pColVal, &timestamp);
        convFunc="ttXlaTimeStampToODBCCType";
      }
      else {
        rc = ttXlaOraTimeStampToODBCTimeStamp(pColVal, &timestamp);
        convFunc="ttXlaOraTimeStampToODBCTimeStamp";
      }
 
      if (rc != SQL_SUCCESS) {
        handleXLAerror (rc, xla_handle, err_buf, &native_error);
        fprintf(stderr, "%s() returns an error <%d>: %s", 
                convFunc, rc, err_buf);
        TerminateGracefully(1);
      }

      printf("  %s: %04d-%02d-%02d %02d:%02d:%02d.%06d\n",
             ((unsigned char*) xla_column_defs[i].colName),
             timestamp.year,timestamp.month, timestamp.day,
             timestamp.hour,timestamp.minute,timestamp.second,
             timestamp.fraction);
    }
 
    /* Read and convert a TimesTen DECIMAL value to a string. */
 
    else if (xla_column_defs[i].dataType == TTXLA_DECIMAL_TT) {
 
      char decimalData[50]; 
      short precision, scale;
      pColVal = (float*) ((unsigned char*) tup + xla_column_defs[i].offset);
      precision = (short) (xla_column_defs[i].precision);
      scale = (short) (xla_column_defs[i].scale);
 
      rc = ttXlaDecimalToCString(pColVal, (char*)&decimalData, precision, scale);
      if (rc != SQL_SUCCESS) {
        handleXLAerror (rc, xla_handle, err_buf, &native_error);
        fprintf(stderr, "ttXlaDecimalToCString() returns an error <%d>: %s", 
                rc, err_buf);
        TerminateGracefully(1);
      }
 
      printf("  %s: %s\n", ((unsigned char*) xla_column_defs[i].colName),
             decimalData);
    }
    else if (xla_column_defs[i].dataType == TTXLA_NUMBER) {
      char numbuf[32];
      pColVal = (void*) ((unsigned char*) tup + xla_column_defs[i].offset);
 
      rc=ttXlaNumberToCString(xla_handle, pColVal, numbuf, sizeof(numbuf));
      if (rc != SQL_SUCCESS) {
        handleXLAerror (rc, xla_handle, err_buf, &native_error);
        fprintf(stderr, "ttXlaNumberToDouble() returns an error <%d>: %s",
                rc, err_buf);
        TerminateGracefully(1);
      }
      printf("  %s: %s\n", ((unsigned char*) xla_column_defs[i].colName), numbuf);
    }
 
  } /* End FOR loop */
}

Note:

  • In the example, var_len is type long, which is a 64-bit (eight-byte) type on UNIX-based 64-bit systems. On Windows 64-bit systems, where long is a four-byte type, __int64 would be used instead.

  • See Terminating an XLA Application for a sample TerminateGracefully() method.

XLA Error Handling

This section discusses XLA error handling.

XLA Errors and Codes

This section documents XLA errors and their error codes.

Table 5-3 XLA Errors and Codes

Error Code

tt_ErrDbAllocFailed

802 (transient)

tt_ErrCondLockConflict

6001 (transient)

tt_ErrDeadlockVictim

6002 (transient)

tt_ErrTimeoutVictim

6003 (transient)

tt_ErrPermSpaceExhausted

6220 (transient)

tt_ErrTempSpaceExhausted

6221 (transient)

tt_ErrBadXlaRecord

8024

tt_ErrXlaBookmarkUsed

8029

tt_ErrXlaLsnBad

8031

tt_ErrXlaNoSQL

8034

tt_ErrXlaNoLogging

8035

tt_ErrXlaParameter

8036

tt_ErrXlaTableDiff

8037

tt_ErrXlaTableSystem

8038

tt_ErrXlaTupleMismatch

8046

tt_ErrXlaDedicatedConnection

8047

How to Handle XLA Errors

Each time you call an ODBC or XLA function, you must check the return code for any errors. If the error is fatal, terminate the program.

See Terminating an XLA Application.

Depending on your application, you may be required to act on specific XLA errors, including those shown in Table 5-3 in the preceding section.

You can check an error using either its error code (error number) or tt_Err string. For the complete list of TimesTen error codes and error strings, see the timesten_home/install/include/tt_errCode.h file. For a description of each message, see List of Errors and Warnings in Oracle TimesTen In-Memory Database Error Messages and SNMP Traps.

If the return code from an XLA function is not SQL_SUCCESS, use the ttXlaError function to retrieve XLA-specific errors on the XLA handle.

Also see Checking for Errors.

The following example, after calling the XLA function ttXlaTableByName, checks to see if the return code is SQL_SUCCESS. If not, it calls an XLA error-handling function followed by a function to terminate the application. See Terminating an XLA Application.

rc = ttXlaTableByName(xla_handle, TABLE_OWNER, TABLE_NAME,
                      &SYSTEM_TABLE_ID, &userID);
if (rc != SQL_SUCCESS) {
  handleXLAerror (rc, xla_handle, err_buf, &native_error);
  fprintf(stderr,
    "ttXlaTableByName() returns an error <%d>: %s", rc, err_buf);
  TerminateGracefully(1);
}

Your XLA error-handling function should repeatedly call ttXlaError until all XLA errors are read from the error stack, proceeding until the return code from ttXlaError is SQL_NO_DATA_FOUND. If you must reread the errors, you can call the ttXlaErrorRestart function to reset the error stack pointer to the first error. (SQL_NO_DATA_FOUND is defined in sqlext.h, which is included by timesten.h.)

The error stack is cleared after a call to any XLA function other than ttXlaError or ttXlaErrorRestart.

Note:

In cases where ttXlaPersistOpen cannot create an XLA handle, it returns the error code SQL_INVALID_HANDLE. Because no XLA handle has been created, ttXlaError cannot be used to detect this error. SQL_INVALID_HANDLE is returned only in cases where no memory can be allocated or the parameters provided are invalid.

The following example shows handleXLAerror(), the error function for the xlaSimple application program.

void handleXLAerror(SQLRETURN rc, ttXlaHandle_h xlaHandle, 
                    SQLCHAR* err_msg, SQLINTEGER* native_error)
{
  SQLINTEGER retLen;
  SQLINTEGER code;
  char* err_msg_ptr;
 
  /* initialize return codes */
  rc = SQL_ERROR;
  *native_error = -1;
  err_msg[0] = '\0';
  
  err_msg_ptr = (char*)err_msg;
  
  while (1)
  {
    int rc = ttXlaError(xlaHandle, &code, err_msg_ptr,
                        ERR_BUF_LEN - (err_msg_ptr - (char*)err_msg), &retLen);
    if (rc == SQL_NO_DATA_FOUND)
    {
      break;
    }
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
      sprintf(err_msg_ptr,
              "*** Error fetching error message via ttXlaError(); rc=<%d>.",rc) ;
      break;
    }
    rc = SQL_ERROR;
    *native_error = code ; 
    /* append any other error messages */
    err_msg_ptr += retLen;
  }
}

Dropping a Table That Has an XLA Bookmark

Before you can drop a table that is subscribed to by an XLA bookmark, you must unsubscribe the table from the bookmark. There are several ways to unsubscribe a table from a bookmark, depending on whether the application is connected to the bookmark.

If XLA applications are connected and using bookmarks that are tracking the table to be dropped, then perform the following tasks.

  1. Each XLA application must call the ttXlaTableStatus function and set the newstatus parameter to 0. This unsubscribes the table from the XLA bookmark in use by the application.

  2. Drop the table.

If XLA applications are not connected and using bookmarks associated with the table to be dropped, then perform the following tasks:

  1. Query the SYS.XLASUBSCRIPTIONS system table to see which bookmarks have subscribed to the table you want to drop.
  2. Use the ttXlaUnsubscribe built-in procedure to unsubscribe the table from each XLA bookmark with a subscription to the table.
  3. Drop the table.

Deleting bookmarks also unsubscribes the table from the XLA bookmarks. See Deleting Bookmarks.

Deleting Bookmarks

You may want to delete bookmarks when you terminate an application or drop a table.

Use the ttXlaDeleteBookmark function to delete XLA bookmarks if the application is connected and using the bookmarks.

As described in About XLA Bookmarks, a bookmark may be reused by a new connection after its previous connection has closed. The new connection can resume reading from the transaction log from where the previous connection stopped. Note the following:

  • If you delete the bookmark, subsequent checkpoint operations such as the ttCkpt or ttCkptBlocking built-in procedure free the file system space associated with any unread update records in the transaction log.

  • If you do not delete the bookmark, when an XLA application connects and reuses the bookmark, all unread update records that have accumulated since the program terminated are read by the application. This is because the update records are persistent in the TimesTen transaction log. However, the danger is that these unread records can build up in the transaction log files and consume a lot of file system space.

Note:

  • You cannot delete replicated bookmarks while the replication agent is running.

  • When you reuse a bookmark, you start with the Initial Read log record identifier in the transaction log file. To ensure that a connection that reuses a bookmark begins reading where the prior connection left off, the prior connection should call ttXlaAcknowledge to reset the bookmark position to the currently accessed record before disconnecting.

  • See ttLogHolds in Oracle TimesTen In-Memory Database Reference. That TimesTen built-in procedure returns information about transaction log holds.

  • Be aware that ttCkpt and ttCkptBlocking require ADMIN privilege. TimesTen built-in procedures and any required privileges are documented in Built-In Procedures in Oracle TimesTen In-Memory Database Reference.

The InitHandler() function in the xlaSimple application deletes the XLA bookmark upon exit, as shown in the following example.

if (deleteBookmark) {
    ttXlaDeleteBookmark(xla_handle);
    if (rc != SQL_SUCCESS) {
    /* See XLA Error Handling */
    }
    xla_handle = NULL; /* Deleting the bookmark has the */
                       /* effect of disconnecting from XLA. */
}
/* Close the XLA connection as described in the next section, 
Terminating an XLA Application. */

If the application is not connected and using the XLA bookmark, you can delete the bookmark either of the following ways:

  • Close the bookmark and call the ttXlaBookmarkDelete built-in procedure.

  • Close the bookmark and use the ttIsql command xladeletebookmark.

Terminating an XLA Application

When your XLA application has finished reading from the transaction log, gracefully exit by rolling back uncommitted transactions and freeing all handles.

There are two approaches to this:

  • Unsubscribe from all tables and materialized views, delete the XLA bookmark, and disconnect from the database.

Or:

  • Disconnect from the database but keep the XLA bookmark in place. When you reconnect at a later time, you can resume reading records from the bookmark.

For the first approach, complete the following steps.

  1. Call ttXlaTableStatus to unsubscribe from each table and materialized view, setting the newstatus parameter to 0.
  2. Call ttXlaDeleteBookmark to delete the bookmark. See Deleting Bookmarks.
  3. Call ttXlaClose to disconnect the XLA handle.
  4. Call the ODBC function SQLTransact with the SQL_ROLLBACK setting to roll back any uncommitted transaction.
  5. Call the ODBC function SQLDisconnect to disconnect from the TimesTen database.
  6. Call the ODBC function SQLFreeConnect to free memory allocated for the ODBC connection handle.
  7. Call the ODBC function SQLFreeEnv to free the ODBC environment handle.

For the second approach, maintaining the bookmark, skip the first two steps but complete the remaining steps.

Be aware that resources should be freed in reverse order of allocation. For example, the ODBC environment handle is allocated before the ODBC connection handle, so for cleanup free the connection handle before the environment handle.

This example shows TerminateGracefully(), the termination function in the xlaSimple application.

void TerminateGracefully(int status)
{
 
  SQLRETURN     rc;
  SQLINTEGER    native_error ; 
  SQLINTEGER    oldstatus;
  SQLINTEGER    newstatus = 0;
    
  /* If the table has been subscribed to through XLA, unsubscribe it. */
 
  if (SYSTEM_TABLE_ID != 0) {
    rc = ttXlaTableStatus(xla_handle, SYSTEM_TABLE_ID, 0,
                          &oldstatus, &newstatus);
    if (rc != SQL_SUCCESS) {
      handleXLAerror (rc, xla_handle, err_buf, &native_error);
      fprintf(stderr, "Error when unsubscribing from "TABLE_OWNER"."TABLE_NAME
              " table <%d>: %s", rc, err_buf);
    }
    SYSTEM_TABLE_ID = 0;
  }
 
  /* Close the XLA connection. */
 
  if (xla_handle != NULL) {
    rc = ttXlaClose(xla_handle);
    if (rc != SQL_SUCCESS) {
      fprintf(stderr, "Error when disconnecting from XLA:<%d>", rc);
    }
    xla_handle = NULL;
  }
 
  if (hstmt != SQL_NULL_HSTMT) {
    rc = SQLFreeStmt(hstmt, SQL_DROP);
    if (rc != SQL_SUCCESS) {
      handleError(rc, henv, hdbc, hstmt, err_buf, &native_error);
      fprintf(stderr, "Error when freeing statement handle:\n%s\n", err_buf);
    }
    hstmt = SQL_NULL_HSTMT;
  }
 
  /* Disconnect from TimesTen entirely. */
    
  if (hdbc != SQL_NULL_HDBC) {
    rc = SQLTransact(henv, hdbc, SQL_ROLLBACK);
    if (rc != SQL_SUCCESS) {
      handleError(rc, henv, hdbc, hstmt, err_buf, &native_error);
      fprintf(stderr, "Error when rolling back transaction:\n%s\n", err_buf);
    }
 
    rc = SQLDisconnect(hdbc);
    if (rc != SQL_SUCCESS) {
      handleError(rc, henv, hdbc, hstmt, err_buf, &native_error);
      fprintf(stderr, "Error when disconnecting from TimesTen:\n%s\n", err_buf);
    }
 
    rc = SQLFreeConnect(hdbc);
    if (rc != SQL_SUCCESS) {
      handleError(rc, henv, hdbc, hstmt, err_buf, &native_error);
      fprintf(stderr, "Error when freeing connection handle:\n%s\n", err_buf);
    }
    hdbc = SQL_NULL_HDBC;
  }
 
  if (henv != SQL_NULL_HENV) {
    rc = SQLFreeEnv(henv);
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
      handleError(rc, henv, hdbc, hstmt, err_buf, &native_error);
      fprintf(stderr, "Error when freeing environment handle:\n%s\n", err_buf);
    }
    henv = SQL_NULL_HENV;
  }
  exit(status);
}