/**********************************************************************
* FILENAME :        BulkOperations.c
*
* DESCRIPTION :
*       Example illustrates bulk reads/updates/deletes/inserts using bookmarks
*       with SQLBulkOperations()
*
*           Keyset-driven            - via SQL_ATTR_CURSOR_TYPE
*           Row-wise binding         - via SQL_ATTR_ROW_BIND_TYPE
*           Record array size of 3   - via SQL_ATTR_ROW_ARRAY_SIZE
*           Variable length bookmark - via SQL_ATTR_USE_BOOKMARKS
*
*           Example used table with no identity field.
*
* ODBC USAGE :
*       Allocates a CustStruct array with 12 elements giving 4 groups of
*       3 records.
*
*       1-3  holds current records selected
*       4-6  holds records to be updated
*       7-9  holds records to be inserted
*       9-12 holds records to be deleted
*
*       SQLSetStmtAttr - to set SQL_ATTR_CURSOR_TYPE,
*                               SQL_ATTR_ROW_BIND_TYPE,
*                               SQL_ATTR_ROW_ARRAY_SIZE,
*                               SQL_ATTR_USE_BOOKMARKS,
*                               SQL_ATTR_ROW_STATUS_PTR,
*                               SQL_ATTR_ROW_BIND_OFFSET_PTR,
*                               SQL_ATTR_ROWS_FETCHED_PTR,
*                               SQL_ATTR_CONCURRENCY.
*       SQLBindCol - to bind columns into CustArray
*       SQLExecDirect - to execute SELECT
*       SQLFetchScroll - to retrieve 1st set of up to 3 records
*
*       Loops until exit:
*           Requests user to select next action from :
*                   SQL_FETCH_NEXT, SQL_FETCH_PRIOR, SQL_FETCH_FIRST,
*                   SQL_FETCH_LAST, SQL_FETCH_ABSOLUTE, SQL_FETCH_RELATIVE
*                   UPDATE_ROW, DELETE_ROW, ADD_ROW, SHOW_ALL or
*                   SEND_TO_DATA_SOURCE
*
*           For SQL_FETCH_NEXT, SQL_FETCH_PRIOR, SQL_FETCH_FIRST,
*               SQL_FETCH_LAST, SQL_FETCH_ABSOLUTE, SQL_FETCH_RELATIVE
*               Calls SQLFetchScroll() to get the next/prior/first/last etc
*               set of 3 records, replacing the first 3 rows in the array
*
*           For  UPDATE_ROW, DELETE_ROW, ADD_ROW
*               Calls GetNewCustData() to request a row to update/delete with
*               new values and bookmark, or to add, saving in the appropriate
*               part of array prior to sending to DB.
*
*           For  SHOW_ALL
*               Dumps out the current 12 rows in the CustArray, showing what
*               is in place for Current/Update/Delete/Add
*
*           For SEND_TO_DATA_SOURCE
*               For each of the 3 areas with pending data (Update/Delete/Add)
*               calls SQLSetStmtAttr with SQL_ATTR_ROW_ARRAY_SIZE so set the
*               number of rows in play and SQLBulkOperations to perform bulk
*               update
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <sql.h>
#include <sqlext.h>
#include <string.h>

#include "util.c"

#define NEXT 1
#define FIRST 2
#define LAST 3
#define PRIOR 4
#define ABSOLUTE 5
#define RELATIVE 6

#define UPDATE_ROW 100
#define DELETE_ROW 101
#define ADD_ROW 102
#define SEND_TO_DATA_SOURCE 103
#define SHOW_ALL 104
#define CURRENT_OFFSET 0
#define UPDATE_OFFSET 3
#define INSERT_OFFSET 6
#define DELETE_OFFSET 9
#define QUIT 0
#define TRUE 1
#define FALSE 0

#define BOOKMARK_LEN  10
#define PERSONID_LEN  2
#define LASTNAME_LEN  255
#define FIRSTNAME_LEN 255
#define ADDRESS_LEN 255
#define CITY_LEN  255

#define DATA_ARRAY_SIZE 12

// Define structure for data
typedef struct tagCustStruct {
    SQLCHAR Bookmark[BOOKMARK_LEN];
    SQLLEN BookmarkLen;
    SQLUINTEGER PersonID;
    SQLLEN PersonIDInd;
    SQLCHAR FirstName[255];
    SQLLEN FirstNameLenOrInd;
    SQLCHAR LastName[255];
    SQLLEN LastNameLenOrInd;
    SQLCHAR Address[255];
    SQLLEN AddressLenOrInd;
    SQLCHAR City[255];
    SQLLEN CityLenOrInd;
} CustStruct;

// Allocate 12 of CustStruct.
// elements 0-2  are for the current rowset,
// elements 3-5  are for the buffered updates,
// elements 6-8  are for the buffered inserts,
// elements 9-11 are for the buffered deletes.
CustStruct CustArray[DATA_ARRAY_SIZE];

// RowStatusArray values (for reference)
// SQL_ROW_SUCCESS                  0
// SQL_ROW_DELETED                  1
// SQL_ROW_UPDATED                  2
// SQL_ROW_NOROW                    3
// SQL_ROW_ADDED                    4
// SQL_ROW_ERROR                    5
// (ODBCVER >= 0x0300)
// SQL_ROW_SUCCESS_WITH_INFO        6
// SQL_ROW_PROCEED	            0
// SQL_ROW_IGNORE		    1

SQLUSMALLINT RowStatusArray[DATA_ARRAY_SIZE], Action, RowNum;
SQLLEN NumUpdates = 0, NumInserts = 0, NumDeletes = 0;
SQLLEN BindOffset = 0;
SQLLEN RowsFetched = 0;
SQLLEN Concurrency = SQL_CONCUR_LOCK;
SQLLEN rowCount;

SQLHENV  henv  = SQL_NULL_HENV;   	// Environment
SQLHDBC  hdbc  = SQL_NULL_HDBC;   	// Connection handle
SQLHSTMT hstmt = SQL_NULL_HSTMT;  	// Statement handle
SQLRETURN retcode;			// Return status

//
// Display customer data array
//
void DisplayCustData(CustStruct *CustArray,
                     SQLUSMALLINT start,
                     SQLUSMALLINT rows) {

    int i;
    char tmp[256];

    if (start==CURRENT_OFFSET) printf ("Current Data:\n");
    if (start==UPDATE_OFFSET) printf ("Update Data:\n");
    if (start==INSERT_OFFSET) printf ("Insert Data:\n");
    if (start==DELETE_OFFSET) printf ("Delete Data:\n");
    for (i=0; i<rows; i++) {
        printf ("Bookmark %.10s, ",
                                (char *)CustArray[start+i].Bookmark);
        printf ("%i ,", (int)CustArray[start+i].PersonID);
        printf ("%.10s ,", (char *)CustArray[start+i].FirstName);
        printf ("%.10s ,", (char *)CustArray[start+i].LastName);
        printf ("%.10s ,", (char *)CustArray[start+i].Address);
        printf ("%.10s\n", (char *)CustArray[start+i].City);
    }
    return;
}

//
// Function to get Action and Row
// Action = 1 - NEXT,2 - FIRST,3 - LAST,4 - PRIOR,5 - ABSOLUTE,6 - RELATIVE,
//          100 - UPDATE_ROW, 101 - DELETE_ROW, 102 - ADD_ROW,
//          103 - SEND_TO_DATA_SOURCE, 104 - SHOW_ALL
int GetAction(CustStruct *CustArray,
              SQLUSMALLINT *StatusArray,
              SQLUSMALLINT *Action,
              SQLUSMALLINT* RowNum) {

    char reply;
    // Display cust data array
    printf ("Current Array: \n");
    DisplayCustData(CustArray, CURRENT_OFFSET, 3);
    printf ("Update Array :\n");
    DisplayCustData(CustArray, UPDATE_OFFSET,  3);
    printf ("Insert Array :\n");
    DisplayCustData(CustArray, INSERT_OFFSET,  3);
    printf ("Delete Array :\n");
    DisplayCustData(CustArray, DELETE_OFFSET,  3);

    printf ("Action : QUIT(0) NEXT(1) FIRST(2)");
    printf ("LAST(3) PRIOR(4) ABS(5) REL(6)\n");
    printf ("........ ROW: UPDATE(100) DELETE(101)");
    printf ("ADD(102) SEND(103) SHOW(104)\n");
    reply=getInt ("Select Action", (int *) Action, 'N', 0);
    if ((*Action == ABSOLUTE || *Action == RELATIVE) && *Action != QUIT) {
        printf ("RowNum : ");
        reply=getInt ("RowNum", (int *) RowNum, 'N', 0);
    }
    if (*Action == QUIT) return 0; else return 1;
}

//
// Clear Buffers
//
void clearBuffers (CustStruct *CustArray, int start, int rows) {

    int i;
    for (i=0;i<rows;i++) {
        memset(CustArray[start+i].Bookmark, ' ', BOOKMARK_LEN);
        CustArray[start+i].PersonID=0;
        memset(CustArray[start+i].FirstName, ' ', FIRSTNAME_LEN);
        memset(CustArray[start+i].LastName, ' ', LASTNAME_LEN);
        memset(CustArray[start+i].Address, ' ', ADDRESS_LEN);
        memset(CustArray[start+i].City, ' ', CITY_LEN);
    }
}

//
// Modify customer data in row number of CURRENT
//
SQLUSMALLINT GetNewCustData(CustStruct *CustArray,
                            SQLUSMALLINT destOffset,
                            SQLUSMALLINT base ) {

    int srcOffset;
    SQLCHAR strFirstName[FIRSTNAME_LEN];
    SQLCHAR strLastName[LASTNAME_LEN];
    SQLCHAR strAddress[ADDRESS_LEN];
    SQLCHAR strCity[CITY_LEN];

    char reply=' ';

    printf ("Dest Offset : %i\n", destOffset);

    // Update record
    if (base==UPDATE_OFFSET) {
        reply=getInt ("Enter Src Offset (0,1,2)", &srcOffset, 'N', 0);
        if (srcOffset<3) {
            //copy CURRENT record to UPDATE area
            memcpy(CustArray[destOffset].FirstName,
                   CustArray[srcOffset].FirstName, FIRSTNAME_LEN);
            memcpy(CustArray[destOffset].LastName,
                   CustArray[srcOffset].LastName, LASTNAME_LEN);
            memcpy(CustArray[destOffset].Address,
                   CustArray[srcOffset].Address, ADDRESS_LEN);
            memcpy(CustArray[destOffset].City,
                   CustArray[srcOffset].City, CITY_LEN);

            // Ask for new FirstName
            printf ("Current First Name : %.20s\n",
                                    CustArray[srcOffset].FirstName);
            reply=getStr ("New First Name ", strFirstName,
                                    sizeof(strFirstName), 'N');

            // Move new FirstName into Update area and remove NULL
            memset(CustArray[destOffset].FirstName, ' ', FIRSTNAME_LEN);
            strcpy (CustArray[destOffset].FirstName, strFirstName);
            CustArray[destOffset].FirstName[strlen(strFirstName)]=' ';

            // Set lengths for Update to lengths of fields
            CustArray[destOffset].FirstNameLenOrInd=FIRSTNAME_LEN;
            CustArray[destOffset].PersonIDInd=SQL_COLUMN_IGNORE;
            CustArray[destOffset].LastNameLenOrInd=LASTNAME_LEN;
            CustArray[destOffset].AddressLenOrInd=ADDRESS_LEN;
            CustArray[destOffset].CityLenOrInd=CITY_LEN;

            // Return row number of update record
            return (SQLUSMALLINT) srcOffset+1;
        }
    }

    // Insert record
    if (base==INSERT_OFFSET) {

        // Clear insert buffer
        clearBuffers (CustArray, destOffset, 1);

        // Ask for new FirstName
        reply=getStr ("New First Name ", strFirstName,
                                                   sizeof(strFirstName), 'N');
        // Move new FirstName into Update area and remove NULL
        strcpy (CustArray[destOffset].FirstName, strFirstName);
        CustArray[destOffset].FirstName[strlen(strFirstName)]=' ';

        // Ask for new LastName
        reply=getStr ("New Last Name ", strLastName,
                                                  sizeof(strLastName), 'N');
        // Move new LastName into Update area and remove NULL
        strcpy (CustArray[destOffset].LastName, strLastName);
        CustArray[destOffset].LastName[strlen(strLastName)]=' ';

        // Ask for new Address
        reply=getStr ("New Address ", strAddress,
                                                sizeof(strAddress), 'N');
        // Move new Address into Insert area and remove NULL
        strcpy (CustArray[destOffset].Address, strAddress);
        CustArray[destOffset].Address[strlen(strAddress)]=' ';

        // Ask for new City
        reply=getStr ("New City ", strCity,
                                             sizeof (strCity), 'N');
        // Move new City into Insert area and remove NULL
        strcpy (CustArray[destOffset].City, strCity);
        memset(CustArray[destOffset].FirstName, ' ', FIRSTNAME_LEN);
        CustArray[destOffset].City[strlen(strCity)]=' ';

        // Set lengths for Insert to lengths of fields
        CustArray[destOffset].PersonIDInd=SQL_COLUMN_IGNORE;
        CustArray[destOffset].FirstNameLenOrInd=FIRSTNAME_LEN;
        CustArray[destOffset].LastNameLenOrInd=LASTNAME_LEN;
        CustArray[destOffset].AddressLenOrInd=ADDRESS_LEN;
        CustArray[destOffset].CityLenOrInd=CITY_LEN;

        // Return row number of update record
        return (SQLUSMALLINT) srcOffset+1;
    }

    // Delete record
    if (base==DELETE_OFFSET) {
        reply=getInt ("Enter Src Offset (0,1,2)",
                                                &srcOffset, 'N', 0);
        if (srcOffset<3) {

            // Return row number of record to delete
            printf ("Deleting row %i\n", srcOffset+1);
            return (SQLUSMALLINT) srcOffset+1;
        }
    }

    return -1;
}

int main () {

    int i;

    retcode = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
    CHECK_ERROR(retcode, "SQLAllocHandle(ENV)",
                henv, SQL_HANDLE_ENV);

    retcode = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION,
                                            (SQLPOINTER*)SQL_OV_ODBC3, 0);
    CHECK_ERROR(retcode, "SQLSetEnvAttr(SQL_ATTR_ODBC_VERSION)",
                henv, SQL_HANDLE_ENV);

    retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    CHECK_ERROR(retcode, "SQLAllocHandle(DBC)",
                henv, SQL_HANDLE_ENV);

    retcode = SQLSetConnectAttr(hdbc, SQL_LOGIN_TIMEOUT, (SQLPOINTER)5, 0);
    CHECK_ERROR(retcode, "SQLSetConnectAttr(SQL_LOGIN_TIMEOUT)",
                hdbc, SQL_HANDLE_DBC);

    retcode = SQLConnect(hdbc, (SQLCHAR*) "DATASOURCE", SQL_NTS,
                               (SQLCHAR*) NULL, 0, NULL, 0);
    CHECK_ERROR(retcode, "SQLConnect(SQL_HANDLE_DBC)",
                hdbc, SQL_HANDLE_DBC);

    // Set to autocommit
    retcode = SQLSetConnectAttr(hdbc, SQL_ATTR_AUTOCOMMIT,
                                                    (SQLPOINTER)TRUE, 0);
    CHECK_ERROR(retcode, "SQLSetConnectAttr(SQL_ATTR_AUTOCOMMIT)",
                hdbc, SQL_HANDLE_DBC);

    retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
    CHECK_ERROR(retcode, "SQLAllocHandle(SQL_HANDLE_STMT)",
                hstmt, SQL_HANDLE_STMT);

    // Set the following statement attributes:
    // SQL_ATTR_CURSOR_TYPE:           Keyset-driven
    // SQL_ATTR_ROW_BIND_TYPE:         Row-wise
    // SQL_ATTR_ROW_ARRAY_SIZE:        3
    // SQL_ATTR_USE_BOOKMARKS:         Use variable-length bookmarks
    // SQL_ATTR_ROW_STATUS_PTR:        Points to RowStatusArray
    // SQL_ATTR_ROW_BIND_OFFSET_PTR:   Points to BindOffset
    // SQL_ATTR_CONCURRENCY:   Sets Concurrency (because default is READ ONLY)
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_TYPE,
                             (SQLPOINTER)SQL_CURSOR_KEYSET_DRIVEN, 0);
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_BIND_TYPE,
                             (SQLPOINTER)sizeof(CustStruct), 0);
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_ARRAY_SIZE,
                             (SQLPOINTER)3, 0);
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_USE_BOOKMARKS,
                             (SQLPOINTER)SQL_UB_VARIABLE, 0);
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_STATUS_PTR,
                             RowStatusArray, 0);
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_BIND_OFFSET_PTR,
                             &BindOffset, 0);
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_ROWS_FETCHED_PTR,
                             &RowsFetched,0);
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_CONCURRENCY,
                             (SQLPOINTER)SQL_CONCUR_LOCK ,0);

    // Bind arrays to the bookmark, PersonID, FirstName, LastName, Address,
    // and City columns.
    retcode = SQLBindCol(hstmt, 0, SQL_C_VARBOOKMARK,
                         CustArray[0].Bookmark, sizeof(CustArray[0].Bookmark),
                         &CustArray[0].BookmarkLen);
    retcode = SQLBindCol(hstmt, 1, SQL_C_ULONG,
                         &CustArray[0].PersonID,
                         sizeof(CustArray[0].PersonID),
                         &CustArray[0].PersonIDInd);
    retcode = SQLBindCol(hstmt, 2, SQL_C_CHAR,
                         CustArray[0].FirstName,
                         sizeof(CustArray[0].FirstName),
                         &CustArray[0].FirstNameLenOrInd);
    retcode = SQLBindCol(hstmt, 3, SQL_C_CHAR,
                         CustArray[0].LastName,
                         sizeof(CustArray[0].LastName),
                         &CustArray[0].LastNameLenOrInd);
    retcode = SQLBindCol(hstmt, 4, SQL_C_CHAR,
                         CustArray[0].Address,
                         sizeof(CustArray[0].Address),
                         &CustArray[0].AddressLenOrInd);
    retcode = SQLBindCol(hstmt, 5, SQL_C_CHAR,
                         CustArray[0].City,
                         sizeof(CustArray[0].City),
                         &CustArray[0].CityLenOrInd);

    // Execute a statement to retrieve rows from the Persons table.
    retcode = SQLExecDirect(hstmt,
                (SQLCHAR*)"SELECT PersonID,FirstName,LastName,Address,City FROM TestTBL1", SQL_NTS);
    CHECK_ERROR(retcode, "SQLExecDirect()", hstmt, SQL_HANDLE_STMT);

    // Fetch and display the first 3 rows.
    retcode = SQLFetchScroll(hstmt, SQL_FETCH_FIRST, 0);
    if (retcode == SQL_NO_DATA) {
        printf ("SQL_NO_DATA\n");
    }

    // Call GetAction to get an action and a row number from the user.
    while (GetAction(CustArray, RowStatusArray, &Action, &RowNum)) {
        switch (Action) {
            case SQL_FETCH_NEXT:
            case SQL_FETCH_PRIOR:
            case SQL_FETCH_FIRST:
            case SQL_FETCH_LAST:
            case SQL_FETCH_ABSOLUTE:
            case SQL_FETCH_RELATIVE:
                clearBuffers (CustArray, CURRENT_OFFSET, 3);
                // Fetch and display the requested data.
                retcode = SQLFetchScroll(hstmt, Action, RowNum);

                if (retcode == SQL_SUCCESS ||
                    retcode == SQL_SUCCESS_WITH_INFO) {
                    printf ("Rows Fetched (%i)\n", (int) RowsFetched);
                    for (i=0;i<DATA_ARRAY_SIZE;i++) {
                        printf ("Row %i, status %i\n", i,
                                              RowStatusArray[i]);
                    }
                    if (RowStatusArray[0]==SQL_ROW_DELETED)
                        clearBuffers (CustArray, CURRENT_OFFSET, 1);
                    if (RowStatusArray[1]==SQL_ROW_DELETED)
                        clearBuffers (CustArray, CURRENT_OFFSET+1, 1);
                    if (RowStatusArray[2]==SQL_ROW_DELETED)
                        clearBuffers (CustArray, CURRENT_OFFSET+2, 1);
                } else {
                    if (retcode == SQL_NO_DATA) {
                        printf ("SQL_NO_DATA\n");
                    } else {
                        CHECK_ERROR(retcode, "SQLFetchScroll()",
                                    hstmt, SQL_HANDLE_STMT);
                    }
                }
            break;

            case UPDATE_ROW:
                // Check if we have reached the maximum number of
                // buffered updates.
                if (NumUpdates < 3) {
                    // Get the new customer data and place it in the next
                    // available element of the buffered updates section
                    // of CustArray, copy the bookmark of the row being
                    // updated to the same element, and increment the update
                    // counter. Checking to see we have not already buffered
                    // an update for this row not shown.
                    RowNum = GetNewCustData(CustArray,
                                            UPDATE_OFFSET + NumUpdates,
                                            UPDATE_OFFSET);
                    memcpy(CustArray[UPDATE_OFFSET + NumUpdates].Bookmark,
                           CustArray[RowNum - 1].Bookmark,
                           CustArray[RowNum - 1].BookmarkLen);
                    CustArray[UPDATE_OFFSET + NumUpdates].BookmarkLen =
                                            CustArray[RowNum - 1].BookmarkLen;
                    NumUpdates++;
                } else {
                    printf ("Buffers full.");
                    printf ("Send buffered changes to the data source.");
                }
            break;

            case DELETE_ROW:
                // Check if we have reached the maximum number of buffered
                // deletes.
                if (NumDeletes < 3) {
                    // Copy the bookmark of the row being deleted to the next
                    // available element of the buffered deletes section of
                    // CustArray and increment the delete counter. Checking
                    // to see we have not already buffered an update for this
                    // row not shown.
                    RowNum = GetNewCustData(CustArray,
                                            DELETE_OFFSET + NumDeletes,
                                            DELETE_OFFSET);
                    memcpy(CustArray[DELETE_OFFSET + NumDeletes].Bookmark,
                           CustArray[RowNum - 1].Bookmark,
                           CustArray[RowNum - 1].BookmarkLen);
                    CustArray[DELETE_OFFSET + NumDeletes].BookmarkLen =
                                            CustArray[RowNum - 1].BookmarkLen;

                    NumDeletes++;
                } else {
                    printf ("Buffers full.");
                    printf ("Send buffered changes to the data source.");
                }
                break;

            case ADD_ROW:
                // Reached maximum number of buffered inserts?
                if (NumInserts < 3) {
                    // Get the new customer data and place it in the next
                    // available element of the buffered inserts section of
                    // CustArray and increment insert counter.
                    GetNewCustData (CustArray,
                                    INSERT_OFFSET + NumInserts,
                                    INSERT_OFFSET);
                    NumInserts++;
                } else {
                    printf ("Buffers full.");
                    printf ("Send buffered changes to the data source.");
                }
                break;

            case SHOW_ALL:
                // Dump the Cust Array Section by Section
                // Current Data
                printf ("Current Rows (%i)\n", (int) 3);
                DisplayCustData(CustArray, CURRENT_OFFSET, 3);

                printf ("For Update : (%i)\n", (int) NumUpdates);
                DisplayCustData(CustArray, UPDATE_OFFSET, NumUpdates);

                printf ("For Insert : (%i)\n", (int) NumInserts);
                DisplayCustData(CustArray, INSERT_OFFSET, NumInserts);

                printf ("For Delete : (%i)\n", (int) NumDeletes);
                DisplayCustData(CustArray, DELETE_OFFSET, NumDeletes);
                break;

            case SEND_TO_DATA_SOURCE:
                // If there are any buffered updates, inserts, or deletes, set
                // the array size to that number, set the binding offset to
                // use the data in the buffered update, insert, or delete part
                // of CustArray, and call SQLBulkOperations to do the updates,
                // inserts, or deletes. Because we will never have more than
                // 3 updates, inserts, or deletes, we can use the same row
                // status array.
                if (NumUpdates) {
                    printf ("Updating %i rows\n", (int)NumUpdates);

                    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_ARRAY_SIZE,
                                             (SQLPOINTER) NumUpdates,
                                             (SQLINTEGER) 0);
                    CHECK_ERROR(retcode, "SQLSetStmtAttr(ROW_ARRAY_SIZE)",
                                hstmt, SQL_HANDLE_STMT);

                    // Set start of record (Bind Offset)
                    BindOffset = UPDATE_OFFSET * sizeof(CustStruct);
                    retcode = SQLBulkOperations(hstmt, SQL_UPDATE_BY_BOOKMARK);
                    CHECK_ERROR(retcode, "SQLBulkOperations(hstmt)",
                                henv, SQL_HANDLE_ENV);
                }

                if (NumInserts) {
                    SQLSetStmtAttr (hstmt,
                                    SQL_ATTR_ROW_ARRAY_SIZE,
                                    (SQLPOINTER) NumInserts, 0);
                    BindOffset = INSERT_OFFSET * sizeof(CustStruct);
                    SQLBulkOperations(hstmt, SQL_ADD);
                }

                if (NumDeletes) {
                    SQLSetStmtAttr (hstmt,
                                    SQL_ATTR_ROW_ARRAY_SIZE,
                                    (SQLPOINTER) NumDeletes, 0);
                    BindOffset = DELETE_OFFSET * sizeof(CustStruct);
                    SQLBulkOperations(hstmt, SQL_DELETE_BY_BOOKMARK);
                }

                // If there were any updates, inserts, or deletes, reset the
                // binding offset and array size to their original values.
                if (NumUpdates || NumInserts || NumDeletes) {
                    // SQLRowCount returns the number of rows affected by an
                    // UPDATE, INSERT, or DELETE statement;

                    SQLRowCount (hstmt, &rowCount);
                    printf ("Rows Effected %i\n", (int) rowCount);

                    SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_ARRAY_SIZE,
                                                            (SQLPOINTER)3, 0);

                    // Now buffers updated clean them out
                    clearBuffers (CustArray, UPDATE_OFFSET, 3);
                    clearBuffers (CustArray, INSERT_OFFSET, 3);
                    clearBuffers (CustArray, DELETE_OFFSET, 3);
                    NumUpdates = 0;
                    NumInserts = 0;
                    NumDeletes = 0;
                    BindOffset = 0;
                }
            break;
        }
    }

exit:

    printf ("\nComplete.\n");

    // Free handles
    // Statement
    if (hstmt != SQL_NULL_HSTMT)
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);

    // Connection
    if (hdbc != SQL_NULL_HDBC) {
        SQLDisconnect(hdbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
    }

    // Environment
    if (henv != SQL_NULL_HENV)
        SQLFreeHandle(SQL_HANDLE_ENV, henv);

    return 0;
}

Further information