/**********************************************************************
* FILENAME :        DescribeParamAndCol.c
*
* DESCRIPTION :
*
*       Example prompts user for SQL Statement and uses SQLDescribeParam
*       and SQLDescribeCol to identify parameters and columns. Prompts
*       user to input values for parameters, executes statement and outputs
*       results sets.
*
*       Deals with multiple 'text' fields, either as Parameters or Columns.
*       i.e. longvarchars. Memory is allocated and bound to all columns and
*       parameters. Data is requested from user for parameters. 'text' data
*       parameter lengths are set using SQL_LEN_DATA_AT_EXEC, indicating data
*       for them is acquired at exec time, in chunks.
*
*       NOTE: See comments on cursor being set to DYNAMIC which is done so
*       that 'text' fields can be re-retrieved in full by SQLGetData after the
*       SQLFetch has already returned them initially. The default FORWARD ONLY
*       cursor would not allow this.
*
* ODBC USAGE :
*
*       Prompts user for statement
*           (e.g. INSERT INTO TestTBL4 (name, Memo1, Memo2) VALUES (?, ?, ?) OR
*                 SELECT name, Memo1, Memo2 from TestTBL4, etc)
*       SQLSetStmtAttr() to set cursor type to SQL_CURSOR_DYNAMIC
*       SQLPrepare() to prepare statement
*       For Parameters:-
*           SQLNumParams() to get parameter count
*           SQLDescribeParam() to get DB information on each parameter
*               Allocates storage and uses SQLBindParameter based on parameter
*               data type. Sets 'text' fields to data-at-exec and asks user for
*               other parameter values
*
*       For Column/row data:-
*          SQLNumColumns() to get column count
*          SQLDescribeColumn() for get DB information on each column requested
*              Allocates storage and uses SQLBindColumn based on parameter data
*              type. Sets buffer length to chunk size for 'text' fields, column
*              data size otherwise.
*
*       Execute statement:
*           SQLExecute() for prerared statement
*           For non-SELECTS (i.e. columns is zero) :-
*               while SQL_NEED_DATA returned
*                   SQLParamData() and find column needing more data
*                   Prompts for more data with GetParamValue()
*                   SQLPutData() to write next/last chunk
*           For SELECTS (i.e. columns is non-zero:-
*               While SQLFetch returns ok and not NO_MORE_DATA
*                   Display Non - LONGVARCHAR column data
*                   SQLGetData() to get LONVARCHAR column data
*/
#include <stdio.h>
#include <stdlib.h>
#include <sql.h>
#include <sqlext.h>
#include <string.h>

#include "util.c"

#define MAXFIELDS 20
#define MAXQUERY  100

#define TEXTSIZE  100 	    // How big the text fields are
#define MAXBATCHSIZE 20+2   // Maximum read/write in one go

#define TRUE 1

// (Some) SQL data type codes (JUST FOR REF)
//#define SQL_UNKNOWN_TYPE    0
//#define SQL_CHAR            1
//#define SQL_NUMERIC         2
//#define SQL_DECIMAL         3
//#define SQL_INTEGER         4
//#define SQL_SMALLINT        5
//#define SQL_FLOAT           6
//#define SQL_REAL            7
//#define SQL_DOUBLE          8
//#if (ODBCVER >= 0x0300)
//#define SQL_DATETIME        9
//#endif
//#define SQL_VARCHAR        12

//* One-parameter shortcuts for date/time data types */
//#if (ODBCVER >= 0x0300)
//#define SQL_TYPE_DATE      91
//#define SQL_TYPE_TIME      92
//#define SQL_TYPE_TIMESTAMP 93
//#endif

SQLCHAR         Statement[MAXQUERY];
SQLSMALLINT     NumParams, NumColumns;
SQLSMALLINT     i, DataType, DecimalDigits, Nullable, paramNo;
SQLULEN         bytesRemaining;

//
// Prompts user for input
//
void gStr (char * str, char * prompt, int max) {

    // Ask user for something to prepare
    printf("%s : ", prompt);

    // Get the name, with size limit.
    fgets (str, max, stdin);

    // remove \n
    str[strlen(str)-1] = '\0';
    return;
}

//
// Uses gStr() to prompt user for parameter value
//
void GetParamValue(SQLPOINTER BufferPtr, SQLINTEGER BufferLen,
                   SQLLEN *LenOrInd, SQLSMALLINT ParamNum, SQLSMALLINT DataType) {

    char ParamPrompt[]="Parameter %i";
    char Prompt[sizeof(ParamPrompt)+10];

    SQLINTEGER tmp;

    sprintf (Prompt, ParamPrompt, ParamNum);
    gStr (BufferPtr, Prompt, BufferLen);
    if (DataType != SQL_LONGVARCHAR) {
        *LenOrInd=strlen (BufferPtr);
    }
    return;
}

//
// Allocated buffer to either Column or Parameter data
//
void AllocBuffer(SQLUINTEGER buffSize, SQLPOINTER *Ptr, SQLLEN *BufferLen) {

    *Ptr=malloc (buffSize);
    memset (*Ptr, ' ', buffSize);
    if (BufferLen != NULL) {
        *BufferLen=buffSize;
    }
    return;
}

//
// Returns the parameter number associated with the paramId pointer returned
// by SQLParamData()
//
SQLSMALLINT findParam (SQLSMALLINT NumParams, PTR pParamId,
                       SQLPOINTER PtrArray[]) {

    int i;
    for (i=0; i<NumParams; i++) {
        if (PtrArray[i]==pParamId)
            return i;
    }
    return -1;
}

//
// Program to illustrate SQLDescribeParam() and SQLDescribeCol()
//
int main () {

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

    SQLLEN      status;
    SQLSMALLINT statuslen;
    SQLPOINTER  ParamPtrArray[MAXFIELDS];
    SQLLEN      ParamBufferLenArray[MAXFIELDS];
    SQLLEN      ParamLenOrIndArray[MAXFIELDS];

    SQLPOINTER  ColPtrArray[MAXFIELDS];
    SQLPOINTER  ColNameArray[MAXFIELDS];
    SQLLEN      ColBufferLenArray[MAXFIELDS];
    SQLLEN      ColLenOrIndArray[MAXFIELDS];
    SQLSMALLINT ColDataTypeArray[MAXFIELDS];

    char *Str;
    PTR  pParamID;
    int i, j=0;

    SQLCHAR textBatch [MAXBATCHSIZE];

    SQLCHAR     ColName[255];
    SQLSMALLINT ColNameLen;
    SQLULEN     ColumnSize;
    SQLLEN      siText;

    // initialise pointer arrays so can be freed
    for (i=0;i<MAXFIELDS;i++) {
        ParamPtrArray[i]=NULL;
    }

    for (i=0;i<MAXFIELDS;i++) {
        ColPtrArray[i]=NULL;
    }

    // Prompt the user for an SQL statement and prepare it.
    gStr (Statement, "SQL Statement", MAXQUERY);
    printf  ("Statement is : %s\n", Statement);

    // Allocate the ODBC environment and save handle.
    retcode = SQLAllocHandle (SQL_HANDLE_ENV, NULL, &henv);
    CHECK_ERROR(retcode, "SQLAllocHandle(SQL_HANDLE_ENV)",
                henv, SQL_HANDLE_ENV);

    // Notify ODBC that this is an ODBC 3.0 app.
    retcode = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION,
                            (SQLPOINTER) SQL_OV_ODBC3, 0);
    CHECK_ERROR(retcode, "SQLSetEnvAttr(SQL_ATTR_ODBC_VERSION)",
                henv, SQL_HANDLE_ENV);

    // Allocate ODBC connection handle and connect.
    retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    CHECK_ERROR(retcode, "SQLAllocHandle(SQL_HANDLE_DBC)",
                hdbc, SQL_HANDLE_DBC);

    // Connect to the driver
    retcode=SQLDriverConnect(hdbc, NULL, "DSN=DATASOURCE;", SQL_NTS,
                             NULL, 0, NULL, SQL_DRIVER_COMPLETE);
    CHECK_ERROR(retcode, "SQLDriverConnect(DATASOURCE)",
                hdbc, SQL_HANDLE_DBC);

    // Allocate statement handle.
    retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
    CHECK_ERROR(retcode, "SQLAllocHandle(SQL_HANDLE_STMT)",
                hstmt, SQL_HANDLE_STMT);

    //
    // Set cursor type to DYNAMIC.
    // To get the full content of 'text' column, we use SQLFetch followed by
    // SQLGetData. For this to work however, (unless the text field is the
    // last column in the statement) we have to set the cursor to either
    // DYMANIC or STATIC because the cursor has to effectively 'go back' to
    // the field to get the data again after SQLFetch has retrieved the
    // first chunk and moved the cursor forward in the first read.
    // Note: If the cursor is left as FORWARD and the 'text' column is the
    // last column in the statement, it will work because the cursor still
    // moving forward and not advance to the next record.
    //
    retcode = SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_TYPE,
                             (SQLPOINTER) SQL_CURSOR_DYNAMIC, 0);

    printf ("Preparing : %s\n", Statement);

    retcode = SQLPrepare(hstmt, Statement, SQL_NTS);
    CHECK_ERROR(retcode, "SQLPrepare(Statement)",
                hstmt, SQL_HANDLE_STMT);

    printf("SQLPrepare(hstmt, Statement, SQL_NTS) OK\n");

    // dumpDescriptors ("HSTMT", hstmt, 'Y', 'Y', 1);
    // Check to see if there are any parameters. If so, process them.
    SQLNumParams(hstmt, &NumParams);
    if (NumParams) {

        printf ("Num Params : %i\n", NumParams);
        for (i = 0; i < NumParams; i++) {
            // Describe the parameter.
            retcode = SQLDescribeParam(hstmt,
                                       i+1,
                                       &DataType,
                                       &bytesRemaining,
                                       &DecimalDigits,
                                       &Nullable);

            CHECK_ERROR(retcode, "SQLPrepare(Statement)",
                        hstmt, SQL_HANDLE_STMT);

            printf("\nSQLDescribeParam() OK\n");
            printf("Data Type : %i, bytesRemaining : %i, DecimalDigits : %i, Nullable %i\n",
                                        (int)DataType, (int)bytesRemaining,
                                        (int)DecimalDigits, (int)Nullable);

            if (DataType==SQL_LONGVARCHAR) {
                AllocBuffer(MAXBATCHSIZE, &ParamPtrArray[i],
                                          &ParamBufferLenArray[i]);
            } else {
                AllocBuffer(bytesRemaining, &ParamPtrArray[i],
                                            &ParamBufferLenArray[i]);
            }

            printf ("Param Buffer Ptr : %p\n",
                                        (SQLPOINTER *) ParamPtrArray[i]);
            printf ("Param Buffer Len : %i\n",
                                        (int)ParamBufferLenArray[i]);

            // Text field
            // 100 bytes overall total
            // indicates data for the parameter will be sent with SQLPutData
            if (DataType==SQL_LONGVARCHAR)  {
                bytesRemaining=(SDWORD) TEXTSIZE;
                ParamLenOrIndArray[i]=SQL_LEN_DATA_AT_EXEC(bytesRemaining);

                printf ("Binding MEMO field - ");
                printf ("Bytes Remaining : %i, ", (int)bytesRemaining);
                printf ("ParamLenOrIndArray : %i\n", (int)ParamLenOrIndArray[i]);

                retcode = SQLBindParameter(hstmt,           // Statment Handle
                                i+1,                        // Parameter Number
                                SQL_PARAM_INPUT,            // Type is INPUT
                                SQL_C_CHAR,                 // C Data Type
                                SQL_LONGVARCHAR,            // SQL Data Type
                                bytesRemaining,             // Parameter size big
                                0,                          // Decimal Digits
                                ParamPtrArray[i],           // Param value Pointer
                                0,                          // Buffer Length
                                &ParamLenOrIndArray[i]);// Len or Indicator

                // data for this column is grabbed when asked for
            } else {
                // Bind the memory to the parameter. Assume that we only have input parameters.
                retcode = SQLBindParameter(hstmt,
                                i+1,
                                SQL_PARAM_INPUT,
                                SQL_C_CHAR,
                                DataType,
                                bytesRemaining,
                                DecimalDigits,
                                ParamPtrArray[i],
                                ParamBufferLenArray[i],
                                &ParamLenOrIndArray[i]);

                // Can get the data for this column immediately
                GetParamValue(ParamPtrArray[i], ParamBufferLenArray[i],
                                    &ParamLenOrIndArray[i], i+1, DataType);
            }

            CHECK_ERROR(retcode, "SQLBindParameter(SQL_PARAM_INPUT)",
                        hstmt, SQL_HANDLE_STMT);
            printf("SQLBindParameter() OK\n");
        }
    } else {
        printf ("No Params\n");
    }

    // Check to see if there are any Columns. If so, get their details
    SQLNumResultCols(hstmt, &NumColumns);
    if (NumColumns) {

        printf ("Num Columns : %i\n", NumColumns);
        for (i = 0; i < NumColumns; i++) {
            // Describe the parameter.
            retcode = SQLDescribeCol(hstmt,
                                     i+1,
                                     ColName, 255,
                                     &ColNameLen,
                                     &DataType,
                                     &ColumnSize,
                                     &DecimalDigits,
                                     &Nullable);

            CHECK_ERROR(retcode, "SQLDescribeCol()",
                        hstmt, SQL_HANDLE_STMT);

            printf("\nSQLDescribeCol () OK\n");
            printf("Data Type : %i, ColName : %s, DecimalDigits : %i, Nullable %i\n",
                   (int)DataType, ColName, (int)DecimalDigits, (int)Nullable);

            AllocBuffer (ColNameLen+1, &ColNameArray[i], NULL);
            strcpy (ColNameArray[i], ColName);
            ColDataTypeArray[i]=DataType;

            if (DataType==SQL_LONGVARCHAR) {
                AllocBuffer(MAXBATCHSIZE, &ColPtrArray[i],
                                                &ColBufferLenArray[i]);
            } else {
                AllocBuffer(ColumnSize, &ColPtrArray[i],
                                                &ColBufferLenArray[i]);
            }

            printf ("Col Buffer Ptr : %p\n",
                                            (SQLPOINTER *) ColPtrArray[i]);
            printf ("Col Buffer Len : %i\n",
                                            (int)ColBufferLenArray[i]);

            if (DataType==SQL_LONGVARCHAR)  {
                //
                // For LONGVARCHAR Columns, we get the first 'Buffer Length'
                // or chunk of data in the first SQLFetch(). For the rest, we
                // can use SQLGetData which will return the whole column (albeit
                // in 'Buffer Length' chunks). This means by using SQLFetch
                // followed by SQLGetData we actually get the first chuck
                // twice. So, we can either NOT BIND, and SQLFetch wont give
                // us the first chunk or BIND and ignore the first chunk when
                // we use SQLGetData.
                //
                retcode = SQLBindCol (
                                hstmt,                     // Statment Handle
                                i+1,                       // Column Number
                                SQL_C_CHAR,                // C Type
                                ColPtrArray[i],            // Column value Pointer
                                ColBufferLenArray[i],      // Buffer Length
                                &ColLenOrIndArray[i]); // Len or Indicator

            } else {
                // Bind the memory to the parameter.
                retcode = SQLBindCol (
                                hstmt,
                                i+1,
                                SQL_C_CHAR,
                                ColPtrArray[i],
                                ColBufferLenArray[i],
                                &ColLenOrIndArray[i]);
            }

            CHECK_ERROR(retcode, "SQLBindCol()",
                                                    hstmt, SQL_HANDLE_STMT);
            printf("SQLBindCol() OK\n");
        }
    } else {
        printf ("No Columns\n");
    }

    // Execute the statement.
    // If SQLExecute encounters a data-at-execution parameter,
    // it returns SQL_NEED_DATA. The application sends the data using
    // SQLParamData and SQLPutData.

    retcode = SQLExecute (hstmt);
    if ( (retcode!=SQL_NEED_DATA) &&
         (retcode!=SQL_SUCCESS) &&
         (retcode!=SQL_SUCCESS_WITH_INFO) )   {
        CHECK_ERROR(retcode, "SQLExecute()", hstmt, SQL_HANDLE_STMT);
    }

    //
    // Select/Delete or Insert?
    //
    // If no columns assume non-select statement
    if (NumColumns == 0) {

        // Get parameter needing more data
        retcode = SQLParamData(hstmt, &pParamID);
        while (retcode==SQL_NEED_DATA) {

            // Find which parameter
            paramNo = findParam (NumParams, pParamID, ParamPtrArray);
            if (paramNo==-1) {
                printf ("\nParam for more data not found\n");
                goto exit;
            }

            while ((int)bytesRemaining > 0) {
                printf ("Bytes still to write %i\n",
                                            (int) bytesRemaining);
                memset (ParamPtrArray[paramNo], ' ',
                                            ParamBufferLenArray[paramNo]);
                GetParamValue(ParamPtrArray[paramNo],
                              ParamBufferLenArray[paramNo],
                              NULL, paramNo+1, SQL_LONGVARCHAR);
                printf ("Param %i, next %i bytes\n", paramNo+1,
                                        (int)strlen(ParamPtrArray[paramNo]));
                retcode = SQLPutData(hstmt,
                                    (UCHAR *)ParamPtrArray[paramNo],
                                    (SQLLEN) strlen(ParamPtrArray[paramNo]));
                CHECK_ERROR(retcode, "SQLPutData()",
                            hstmt, SQL_HANDLE_STMT);
                bytesRemaining-=strlen(ParamPtrArray[paramNo]);
            }

            bytesRemaining=(SDWORD) TEXTSIZE;

            // Make final SQLParamData call.
            printf ("Final Call - SQLParamData\n");
            retcode = SQLParamData(hstmt, &pParamID);
            if ( (retcode!=SQL_NEED_DATA) &&
                 (retcode!=SQL_SUCCESS) &&
                 (retcode!=SQL_SUCCESS_WITH_INFO) )   {
                CHECK_ERROR(retcode, "SQLParamData()",
                                                hstmt, SQL_HANDLE_STMT);
            }
            printf ("Code = %i (%i)\n", (int)retcode, SQL_NEED_DATA);
        }
    } else {
        // initialise buffers
        for (i=0;i<NumColumns;i++) {
            memset( ColPtrArray[i], ' ', ColBufferLenArray[i]);
            strcpy (ColPtrArray[i], "");
        }

        while ( (retcode = SQLFetch(hstmt)) != SQL_NO_DATA ) {

            CHECK_ERROR(retcode, "SQLFetch()", hstmt, SQL_HANDLE_STMT);

            // Loop round each column, output based on TYPE
            printf ("\n\nRecord %i", ++j);
            for (i=0;i<NumColumns;i++) {
                printf ("\nColumn : %i",   i+1);
                printf ("\nName   : %s",   (char *) ColNameArray[i]);
                printf ("\nType   : %i",   (int) ColDataTypeArray[i]);

                // As we are BINDING LONGVARCHAR columns in this example, we
                // ignore the chunk of data returned by the SQLFetch. We get
                // the whole column regardless.
                if (ColDataTypeArray[i] == SQL_LONGVARCHAR) {
                    printf ("\nValue  : ");
                    while ((retcode = SQLGetData(hstmt,
                                                 i+1,
                                                 SQL_CHAR,
                                                 ColPtrArray[i],
                                                 ColBufferLenArray[i],
                                                 &ColLenOrIndArray[i]))
                                                 != SQL_NO_DATA) {
                        CHECK_ERROR(retcode, "SQLGetData()",
                                    hstmt, SQL_HANDLE_STMT);

                        printf ("\n                  %s",
                                                    (char *) ColPtrArray[i]);
                    }
                } else {
                    printf ("\nValue  : %s",
                                                    (char *) ColPtrArray[i]);
                }
            }
            // re-initialise buffers
            for (i=0;i<NumColumns;i++) {
                memset( ColPtrArray[i], ' ', ColBufferLenArray[i]);
                strcpy (ColPtrArray[i], "");
            }
        }
    }

exit:

    // Free the memory allocated for each parameter and the memory allocated
    // for the arrays of pointers, buffer lengths, and length/indicator values.

    for (i = 0; i < NumParams; i++) {
        if (ParamPtrArray[i]!=NULL) free(ParamPtrArray[i]);
    }

    for (i = 0; i < NumColumns; i++) {
        if (ColPtrArray[i]!=NULL) free(ColPtrArray[i]);
    }

    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