/*-------
 * Module:			test.cpp
 *
 * Description:		This module contains tests for ODBC driver APIs
 *					in an attempt to catch bugs that BI tools may 
 *					run into.
 *
 * Important Note:	These tests are written in C++ using the CPPUnit
 *					framework. 
 *
 * Classes:			rvStrings, getInfoStrings, OdbcDriverTest
 *
 * API functions:	SQLGetDiagRec, SQLConnect, SQLSetConnectAttr , SQLNumResultCols,
 *                  SQLAllocHandle, SQLSetEnvAttr, SQLFreeHandle, SQLDisconnect,
 *                  SQLDataSources, SQLGetInfo, SQLPrepare, SQLBindCol, SQLTables,
 *                  SQLExecDirect, SQLColAttribute, SQLFetch, SQLFreeEnv,
 *                  SQLFreeStmt
 *
 * Comments:		In order to run this suite, you must 
 *                  1. Have cppunit installed
 *                  2. Run sql-trade-store-setup.xml test
 *                  3. Have the ODBC driver installed
 * 
 *                  See "notice.txt" for copyright and license information.
 *-------
 */

#include <iostream>
#include <cppunit/TestSuite.h>
#include <cppunit/TestCaller.h>
#include <cppunit/Test.h>
#include <cppunit/TestFixture.h>
#include <cppunit/ui/text/TestRunner.h>
#include <map>

#include "pgapifunc.h"
#include "psqlodbc.h"

using namespace std;

static const char* DSN = "local-trade-store-odbc-7033";

struct DataBinding {
   SQLSMALLINT TargetType;  
   SQLPOINTER TargetValuePtr;  
   SQLINTEGER BufferLength;  
   SQLLEN StrLen_or_Ind;  
};

struct RowInfo {
    SQLCHAR Name[11];
    SQLINTEGER NameLen;
    SQLCHAR Type[7];
    SQLINTEGER TypeLen;
    SQLCHAR Status;
    SQLINTEGER StatusLenOrInd;
};

class rvStrings {
public:
  rvStrings() {
    intmap.insert(pair<int, string>(0, "SQL_SUCCESS"));
    intmap.insert(pair<int, string>(1, "SQL_SUCCESS_WITH_INFO"));
    intmap.insert(pair<int, string>(100, "SQL_NO_DATA"));
    intmap.insert(pair<int, string>(-1, "SQL_ERROR"));
    intmap.insert(pair<int, string>(-2, "SQL_INVALID_HANDLE"));
  }
  string getString(int i) {
    map<int, string>::iterator rvI = intmap.find(i);
    return ((rvI == intmap.end()) ? "<unknown>" : (*rvI).second);
  }
  map<int, string> intmap;
};

class getInfoStrings {
public:
  getInfoStrings() {
    intmap.insert(pair<int, string>(SQL_HANDLE_DBC, "local-trade-store-odbc-7033"));
    intmap.insert(pair<int, string>(SQL_DRIVER_NAME, "psqlodbcw.so"));
    intmap.insert(pair<int, string>(SQL_SERVER_NAME, "localhost"));
    intmap.insert(pair<int, string>(SQL_DATABASE_NAME, "__MarkLogic"));
    intmap.insert(pair<int, string>(SQL_DBMS_NAME, "MarkLogic"));
    intmap.insert(pair<int, string>(SQL_ACCESSIBLE_TABLES, "Y"));
    intmap.insert(pair<int, string>(SQL_OWNER_TERM, "schema"));
    intmap.insert(pair<int, string>(SQL_TABLE_TERM, "table"));
  }

  map<int, string> intmap;
};

class getTypeInfoStrings {
public:
  getTypeInfoStrings() {
    intmap.insert(pair<int, string>(0, "uuid"));
    intmap.insert(pair<int, string>(1, "-11"));
    intmap.insert(pair<int, string>(2, "37"));
    intmap.insert(pair<int, string>(3, "\'"));
    intmap.insert(pair<int, string>(4, "\'"));
    intmap.insert(pair<int, string>(5, "max. length"));
    intmap.insert(pair<int, string>(6, "1"));
    intmap.insert(pair<int, string>(7, "0"));
    intmap.insert(pair<int, string>(8, "2"));
    intmap.insert(pair<int, string>(9, "0"));
    intmap.insert(pair<int, string>(10, "0"));    
  }

  map<int, string> intmap;
};

void getError(string fn, SQLHANDLE handle, SQLSMALLINT type) {
    SQLINTEGER i = 0;
    SQLINTEGER nativeError;
    SQLCHAR SQLState[SQL_SQLSTATE_SIZE + 1];
    SQLCHAR message[SQL_MAX_MESSAGE_LENGTH];
    SQLSMALLINT textLength;
    SQLRETURN ret;
    rvStrings rvals;
    
    cout << "Ran into an error in function " << fn << endl;

    do {
        ret = SQLGetDiagRec(type, handle, ++i, SQLState, &nativeError, message, sizeof(message) / sizeof(SQLCHAR), &textLength);
        if (SQL_SUCCEEDED(ret)) {
            cout << '\t' << SQLState << ":" << i << ":" << nativeError << " -- " << message << endl;
        }
        else {
            cout << "\t\tSQLGetDiagRec() returned error " << rvals.getString(ret) << endl;
        }
    }
    while (ret == SQL_SUCCESS);
}

void connectToDataSource(SQLHDBC hdbc, char *dsn) {
    SQLRETURN retcode;

    if (retcode = SQLSetConnectAttr(hdbc, SQL_LOGIN_TIMEOUT, (SQLPOINTER) 10, 0)) {
        getError("SQLSetConnectAttr()", hdbc, SQL_HANDLE_DBC);
    }
    if (retcode = SQLConnect(hdbc, (SQLCHAR *) dsn, SQL_NTS, (SQLCHAR *) NULL, 0, NULL, 0)) {
        getError("SQLConnect()", hdbc, SQL_HANDLE_DBC);
    }
}

SQLSMALLINT getNumResultCols(SQLHSTMT hstmt) {
    SQLRETURN retcode;
    SQLSMALLINT numCols;

    if (retcode = SQLNumResultCols(hstmt, (SQLSMALLINT *) &numCols)) {
        getError("SQLNumResultCols(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }
#ifdef DEBUG
    else {
        cout << "Num result cols: " << numCols << endl;
    }
#endif

    return numCols;
}

class ODBCDriverTest : public CppUnit::TestFixture {
private:
    SQLHENV henv = SQL_NULL_HENV;
    SQLHDBC hdbc = SQL_NULL_HDBC;
    SQLHSTMT hstmt = SQL_NULL_HSTMT;
    SQLRETURN retcode;
    SQLCHAR * ret;
    char dsnName[SQL_MAX_DSN_LENGTH];
    char driverDesc[256];
    SQLSMALLINT dsnLen;
    SQLSMALLINT descLen;
    SQLUSMALLINT direction;
    void allocateStmtHandle();
    void runQuery(SQLCHAR *);    
    void bindCols(SQLSMALLINT, DataBinding *);
    void getData(SQLSMALLINT, DataBinding *);

public:
    void setUp();
    void tearDown();

    void connectionTest();
    void getDSNTest();
    void sqlGetInfoTest();
    void sqlGetTypeInfoTest();
    void getTables();
    void getSysTables();
    void runQueries();
    void colAttrTest();
    void getFunctionsTest();
    void bug53242Test();
    void bug55356Test();
    void describeColTest();
};

void ODBCDriverTest::
setUp() {
    cout << endl;
    if (retcode = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv)) {
        getError("SQLAllocHandle(SQL_HANDLE_ENV)", henv, SQL_HANDLE_ENV);
    }
#ifdef DEBUG
    else {
        cout << "Environment handle allocated at " << henv << endl;
    }
#endif
    if (retcode = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0)) {
        getError("SQLSetEnvAttr(SQL_ATTR_ODBC_VERSION)", henv, SQL_HANDLE_ENV);
    }
    if (retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc)) {
        getError("SQLAllocHandle(SQL_HANDLE_DBC)", hdbc, SQL_HANDLE_DBC);
    }
#ifdef DEBUG
    else {
        cout << "Connection handle allocated at  " << hdbc << endl;
    }
#endif
}

void ODBCDriverTest::
tearDown() {
    if (hstmt != SQL_NULL_HSTMT) {
#ifdef DEBUG
        cout << "Freeing handle at               " << hstmt << endl;
#endif
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
    }
    if (hdbc != SQL_NULL_HDBC) {
#ifdef DEBUG
        cout << "Freeing handle at               " << hdbc << endl;
#endif
        SQLDisconnect(hdbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
    }
    if (henv != SQL_NULL_HENV) {
#ifdef DEBUG
        cout << "Freeing handle at               " << henv << endl;
#endif
        SQLFreeHandle(SQL_HANDLE_ENV, henv);
    }
}

void ODBCDriverTest::
allocateStmtHandle() {
    SQLRETURN retcode;

    connectToDataSource(hdbc, (char *) DSN);

    if (retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt)) {
        getError("SQLAllocHandle(SQLHSTMT", hstmt, SQL_HANDLE_STMT);
    }
#ifdef DEBUG
    else {
        cout << "Statement handle allocated at " << hstmt << endl;
    }
#endif
}

void ODBCDriverTest::
bindCols(SQLSMALLINT numCols, DataBinding* data) {
    int bufferSize = MAX_VARCHAR_SIZE;

    for (int i = 0; i < numCols; i++) {
        data[i].TargetType = SQL_C_CHAR;
        data[i].BufferLength = bufferSize + 1;
        data[i].TargetValuePtr = malloc(sizeof(unsigned char) * data->BufferLength);

        if (retcode = SQLBindCol(hstmt, (SQLUSMALLINT) i + 1, data[i].TargetType, 
          data[i].TargetValuePtr, data[i].BufferLength, &data[i].StrLen_or_Ind)) {
            getError("SQLBindCol(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
        }
    }
}

void ODBCDriverTest::
getData(SQLSMALLINT numCols, DataBinding* data) {
    retcode = SQL_SUCCESS;
    while (SQL_SUCCESS == retcode) {
        if (retcode = SQLFetch(hstmt)) {
            getError("SQLFetch(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
            break;
        }
#ifdef DEBUG
        printf("| ");
        for (int j = 0; j < numCols; j++) {
            printf(" %s |", (char *) data[j].TargetValuePtr);
        }
        printf("\n");
#endif
    }
}

void ODBCDriverTest::
runQuery(SQLCHAR* sql) {
    int bufferSize = MAX_VARCHAR_SIZE;
    SQLSMALLINT numCols;

    allocateStmtHandle();

    if (retcode = SQLPrepare(hstmt, sql, SQL_NTS)) {
        getError("SQLPrepare(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }
    numCols = getNumResultCols(hstmt);
    DataBinding *data = new DataBinding[numCols];

    bindCols(numCols, data);

    if (retcode = SQLExecute(hstmt)) {
        getError("SQLExecute(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }

    getData(numCols, data);

    for (int i = 0; i < numCols; i++) {
        free(data[i].TargetValuePtr);
    }
    delete data;
}

void ODBCDriverTest::
getDSNTest() {
    direction = SQL_FETCH_FIRST;
    do {
        retcode = SQLDataSources(henv, direction, (SQLCHAR *) dsnName, sizeof(dsnName), &dsnLen, (SQLCHAR *) driverDesc, sizeof(driverDesc), &descLen);
        if (SQL_SUCCEEDED(retcode)) {
            direction = SQL_FETCH_NEXT;
            cout.write(dsnName,10)<<" = DSN\n";
            cout.write(driverDesc,10)<<" = Description\n";
        }
        else {
            getError("SQLDataSources()", henv, SQL_NULL_HANDLE);
        }
    }
    while (SQL_SUCCEEDED(retcode));
}

void ODBCDriverTest::
connectionTest() {
    SQLCHAR dbmsName[SQL_MAX_COLUMN_NAME_LEN];
    string name = "MarkLogic";

    connectToDataSource(hdbc, (char *) DSN);

    if (retcode = SQLGetInfo(hdbc, SQL_DBMS_NAME, &dbmsName, sizeof(dbmsName), NULL)) {
        getError("SQLGetInfo()", hdbc, SQL_HANDLE_DBC);
    }
    else {
        CPPUNIT_ASSERT(name.compare((string)((char *) dbmsName)) == 0);
    }
}

void ODBCDriverTest::
sqlGetInfoTest() {
#ifdef DEBUG
    cout << SQL_INFO_FIRST << " : " << SQL_INSERT_STATEMENT << endl;
#endif
    SQLCHAR buffer[1024];
    string name = "MarkLogic";

    connectToDataSource(hdbc, (char *) DSN);

    for (int i = SQL_INFO_FIRST; i <= SQL_INSERT_STATEMENT + 2; i++) {   // Go past the end of enums to make sure we get Unrecognized key
        if (retcode = SQLGetInfo(hdbc, i, &buffer, sizeof(buffer), NULL)) {
            getError("SQLGetInfo(SQLHDBC)", hdbc, SQL_HANDLE_DBC);
        }
#ifdef DEBUG
        else {
            cout << i << " = " << buffer << endl;
        }
#endif
    }

    if (retcode = SQLGetInfo(hdbc, SQL_USER_NAME, &buffer, sizeof(buffer), NULL)) {
        getError("SQLGetInfo(SQLHDBC)", hdbc, SQL_HANDLE_DBC);
    }
    else {
        CPPUNIT_ASSERT(((string)(char *)buffer).compare((string)((char *) "admin")) == 0);
    }
    getInfoStrings gIS;
    map<int, string> gisMap = gIS.intmap;

    for (map<int, string>::iterator it = gisMap.begin(); it != gisMap.end(); it++) {
        if (retcode = SQLGetInfo(hdbc, it->first, &buffer, sizeof(buffer), NULL)) {
            getError("SQLGetInfo(SQLHDBC)", hdbc, SQL_HANDLE_DBC);
        }
        else {
            CPPUNIT_ASSERT(((string)(char *)buffer).compare(it->second) == 0);
        }
    }
}

void ODBCDriverTest::
sqlGetTypeInfoTest() {
    SQLSMALLINT numCols;
    int bufferSize = MAX_VARCHAR_SIZE;

    allocateStmtHandle();

    if (retcode = SQLGetTypeInfo(hstmt, SQL_ALL_TYPES)) {
        getError("SQLGetTypeInfo(SQLHSTMT)", hdbc, SQL_HANDLE_STMT);
    }

    numCols = getNumResultCols(hstmt);
    DataBinding *data = new DataBinding[numCols];

    bindCols(numCols, data);

    getData(numCols, data);

    getTypeInfoStrings gtIS;
    map<int, string> gtisMap = gtIS.intmap;

    for (map<int, string>::iterator it = gtisMap.begin(); it != gtisMap.end(); it++) {
        CPPUNIT_ASSERT(((string)(char *)data[it->first].TargetValuePtr).compare(it->second) == 0);
    }

    for (int i = 0; i < numCols; i++) {
        free(data[i].TargetValuePtr);
    }
    delete data;
}

void ODBCDriverTest::
getSysTables() {
    int bufferSize = MAX_VARCHAR_SIZE;
    SQLSMALLINT numCols;
    SQLCHAR sql[] = "select * from sys_tables";

    allocateStmtHandle();

    if (retcode = SQLPrepare(hstmt, sql, SQL_NTS)) {
        getError("SQLPrepare(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }
    numCols = getNumResultCols(hstmt);
    DataBinding *data = new DataBinding[numCols];

    bindCols(numCols, data);

    // if (retcode = SQLExecute(hstmt)) {
    //     getError("SQLExecute(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    // }

    // all tables
    if (retcode = SQLTables(hstmt, NULL, 0, NULL, 0, NULL, 0, NULL, 0)) {
        getError("SQLTables(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }

    // catalogs
    // if (retcode = SQLTables(hstmt, (SQLCHAR *) SQL_ALL_CATALOGS, SQL_NTS, (SQLCHAR *) "", SQL_NTS, (SQLCHAR *) "", SQL_NTS, (SQLCHAR *) "", SQL_NTS)) {
    //     getError("SQLTables(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    // }

    // schemas
    // if (retcode = SQLTables(hstmt, (SQLCHAR *) "", SQL_NTS, (SQLCHAR *) SQL_ALL_SCHEMAS, SQL_NTS, (SQLCHAR *) "", SQL_NTS, (SQLCHAR *) "", SQL_NTS)) {
    //     getError("SQLTables(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    // }

    // table types returns nothing
    // if (retcode = SQLTables(hstmt, (SQLCHAR *) "", SQL_NTS, (SQLCHAR *) "", SQL_NTS, (SQLCHAR *) "", SQL_NTS, (SQLCHAR *) SQL_ALL_TABLE_TYPES, SQL_NTS)) {
    //     getError("SQLTables(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    // }
    
    getData(numCols, data);
    CPPUNIT_ASSERT(((string)(char *) data[3].TargetValuePtr).compare("TABLE") == 0);
    CPPUNIT_ASSERT(((string)(char *) data[0].TargetValuePtr).compare("__MarkLogic") == 0);

    for (int i = 0; i < numCols; i++) {
        free(data[i].TargetValuePtr);
    }
    delete data;
}

void ODBCDriverTest::
getTables() {
    int bufferSize = 1024;
    SQLSMALLINT numCols;
    SQLCHAR sql[] = "select name, type from sys_tables";

    allocateStmtHandle();

    if (retcode = SQLExecDirect(hstmt, sql, SQL_NTS)) {
        getError("SQLExecDirect(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }
    numCols = getNumResultCols(hstmt);

    CPPUNIT_ASSERT(numCols == 2);
}

void ODBCDriverTest::
runQueries() {
    runQuery((SQLCHAR *) "select * from customer order by customerid");
    runQuery((SQLCHAR *) "select distinct customerid, customer.traderid \
      from customer join trader on trader.traderid order by customerid");
}

void ODBCDriverTest::
colAttrTest() {
    allocateStmtHandle();

    if (retcode = SQLExecDirect(hstmt, 
      (SQLCHAR *) "select name, type from sys_tables", SQL_NTS)) {
        getError("SQLExecDirect", hstmt, SQL_HANDLE_STMT);
    }
    else {
        char buf[64];
        if (retcode = SQLColAttribute(hstmt, 1, SQL_DESC_LABEL, buf, sizeof(buf), NULL, NULL)) {
            getError("SQLColAttribute", hstmt, SQL_HANDLE_STMT);
        }
        else {
            CPPUNIT_ASSERT(((string) buf).compare("name") == 0);
        }
        memset(buf, 0, sizeof(buf));
        if (retcode = SQLColAttribute(hstmt, 1, SQL_DESC_LOCAL_TYPE_NAME, buf, sizeof(buf), NULL, NULL)) {
            getError("SQLColAttribute", hstmt, SQL_HANDLE_STMT);
        }
        else {
            CPPUNIT_ASSERT(((string) buf).compare("varchar") == 0);
        }
    }
}

void ODBCDriverTest::
getFunctionsTest() {
    allocateStmtHandle();

    SQLUSMALLINT funs[SQL_API_ODBC3_ALL_FUNCTIONS_SIZE];

    if (retcode = SQLGetFunctions(hstmt, SQL_API_ODBC3_ALL_FUNCTIONS, funs)) {
        getError("SQLGetFunctions", hstmt, SQL_HANDLE_STMT);
    }

    for (int i = 0; i < SQL_API_ODBC3_ALL_FUNCTIONS_SIZE; i++) {
        cout << funs[i] << ", " ;
    }
}

void ODBCDriverTest::
bug53242Test() {
    // Set up the ODBC environment
    SQLRETURN ret;
    SQLHENV hdlEnv;
    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hdlEnv); 
    if(!SQL_SUCCEEDED(ret)) {
        printf("Could not allocate a handle.\n");
    } else {
        printf("Allocated an environment handle.\n");
    }
    // Tell ODBC that the application uses ODBC 3.
    ret = SQLSetEnvAttr(hdlEnv, SQL_ATTR_ODBC_VERSION,
        (SQLPOINTER) SQL_OV_ODBC3, SQL_IS_UINTEGER);
    if(!SQL_SUCCEEDED(ret)) {
        printf("Could not set application version to ODBC3.\n");
    } else {
        printf("Set application to ODBC 3.\n");
    }
    // Allocate a database handle.
    SQLHDBC hdlDbc;
    ret = SQLAllocHandle(SQL_HANDLE_DBC, hdlEnv, &hdlDbc); 
    if(!SQL_SUCCEEDED(ret)) {
        printf("Could not allocate database handle.\n");
    } else {
        printf("Allocated Database handle.\n");
    }
    // Connect to the database
    printf("Connecting to database.\n");
    const char *dsnName = DSN;
    const char* userID = "admin";
    const char* passwd = "admin";
    ret = SQLConnect(hdlDbc, (SQLCHAR*)dsnName,
        SQL_NTS,(SQLCHAR*)userID,SQL_NTS,
        (SQLCHAR*)passwd, SQL_NTS);
    if(!SQL_SUCCEEDED(ret)) {
        printf("Could not connect to database.\n");
    } else {
        printf("Connected to database.\n");
    }
    // Get the AUTOCOMMIT state
    SQLINTEGER  autoCommitState;
    SQLGetConnectAttr(hdlDbc, SQL_ATTR_AUTOCOMMIT, &autoCommitState, 0, NULL);
    printf("Autocommit is set to: %d\n", autoCommitState);    

    // Set up a statement handle
    SQLHSTMT hdlStmt;
    SQLAllocHandle(SQL_HANDLE_STMT, hdlDbc, &hdlStmt);
    
    // Disable AUTOCOMMIT
    printf("Disabling autocommit.\n");
    ret = SQLSetConnectAttr(hdlDbc, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, 
        SQL_NTS);
    if(!SQL_SUCCEEDED(ret)) {
        printf("Could not disable autocommit.\n");
    }
    
    // Get the AUTOCOMMIT state again
    SQLGetConnectAttr(hdlDbc, SQL_ATTR_AUTOCOMMIT, &autoCommitState, 0, NULL);

    // Create a table to hold the data
    ret = SQLExecDirect(hdlStmt, (SQLCHAR*)"SELECT name from sys.sys_tables",
        SQL_NTS);
 
    if(!SQL_SUCCEEDED(ret)) {
        printf("Could not perform single select.\n");
    } else {
        printf("Performed single select.\n");
    }
    
    // Need to commit the transaction before closing, since autocommit is 
    // disabled. Otherwise SQLDisconnect returns an error.
    printf("Committing transaction.\n");
    ret =  SQLEndTran(SQL_HANDLE_DBC, hdlDbc, SQL_COMMIT);
    if(!SQL_SUCCEEDED(ret)) {
        printf("Error committing transaction.\n");
    }     
    
    // Clean up
    printf("Free handles.\n");
    ret = SQLDisconnect(hdlDbc);
    if(!SQL_SUCCEEDED(ret)) {
        printf("Error disconnecting from database. Transaction still open?\n");
    }    
    SQLFreeHandle(SQL_HANDLE_STMT, hdlStmt);
    SQLFreeHandle(SQL_HANDLE_DBC, hdlDbc); 
    SQLFreeHandle(SQL_HANDLE_ENV, hdlEnv);  
    exit(0);
}

void ODBCDriverTest::
bug55356Test() {
    allocateStmtHandle();
    
    if (retcode = SQLPrepare(hstmt, (SQLCHAR *)"select * from sys_tables", SQL_NTS)) {
        getError("SQLPrepare(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }

    SQLCHAR *      ColumnName[20];
    SQLSMALLINT    ColumnNameLen[50];
    SQLSMALLINT    ColumnDataType[20];
    SQLULEN        ColumnDataSize[20];
    SQLSMALLINT    ColumnDataDigits[20];
    SQLSMALLINT    ColumnDataNullable[20];
    SQLSMALLINT    i,numCols;

    // Retrieve number of columns
    if( retcode = SQLNumResultCols(hstmt, &numCols)) {
        getError("SQLNumResultCols(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }

    printf ("Number of Result Columns %i\n", numCols);

    // Loop round number of columns using SQLDescribeCol to get info about
    // the column, followed by SQLBindCol to bind the column to a data area
    for (i=0;i<numCols;i++) {
        ColumnName[i] = (SQLCHAR *) malloc (50);
        if( retcode = SQLDescribeCol (
            hstmt,                    // Select Statement (Prepared)
            i+1,                      // Columnn Number
            ColumnName[i],            // Column Name (returned)
            50,                       // size of Column Name buffer
            &ColumnNameLen[i],        // Actual size of column name
            &ColumnDataType[i],       // SQL Data type of column
            &ColumnDataSize[i],       // Data size of column in table
            &ColumnDataDigits[i],     // Number of decimal digits
            &ColumnDataNullable[i])   // Whether column nullable
        ) {
            getError("SQLDescribeCol(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
        }

        // Display column data
        printf("\nColumn : %i\n", i+1);
        printf("Column Name : %s\n  Column Name Len : %i\n  SQL Data Type : %i\n  Data Size : %i\n  DecimalDigits : %i\n  Nullable %i\n",
                 ColumnName[i], (int)ColumnNameLen[i], (int)ColumnDataType[i],
                 (int)ColumnDataSize[i], (int)ColumnDataDigits[i],
                 (int)ColumnDataNullable[i]);
    }
}

void ODBCDriverTest::
describeColTest() {
    allocateStmtHandle();
    
    if (retcode = SQLPrepare(hstmt, (SQLCHAR *)"select * from sys_tables", SQL_NTS)) {
        getError("SQLPrepare(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }

    SQLCHAR *      ColumnName[20];
    SQLSMALLINT    ColumnNameLen[50];
    SQLSMALLINT    ColumnDataType[20];
    SQLULEN        ColumnDataSize[20];
    SQLSMALLINT    ColumnDataDigits[20];
    SQLSMALLINT    ColumnDataNullable[20];
    SQLSMALLINT    i,numCols;

    // Retrieve number of columns
    if( retcode = SQLNumResultCols(hstmt, &numCols)) {
        getError("SQLNumResultCols(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
    }

    printf ("Number of Result Columns %i\n", numCols);
    CPPUNIT_ASSERT(numCols==6);

    // Loop round number of columns using SQLDescribeCol to get info about
    // the column, followed by SQLBindCol to bind the column to a data area
    for (i=0;i<numCols;i++) {
        ColumnName[i] = (SQLCHAR *) malloc (50);
        if( retcode = SQLDescribeCol (
            hstmt,                    // Select Statement (Prepared)
            i+1,                      // Columnn Number
            ColumnName[i],            // Column Name (returned)
            50,                       // size of Column Name buffer
            &ColumnNameLen[i],        // Actual size of column name
            &ColumnDataType[i],       // SQL Data type of column
            &ColumnDataSize[i],       // Data size of column in table
            &ColumnDataDigits[i],     // Number of decimal digits
            &ColumnDataNullable[i])   // Whether column nullable
        ) {
            getError("SQLDescribeCol(SQLHSTMT)", hstmt, SQL_HANDLE_STMT);
        }

        // Display column data
        printf("\nColumn : %i\n", i+1);
        printf("Column Name : %s\n  Column Name Len : %i\n  SQL Data Type : %i\n  Data Size : %i\n  DecimalDigits : %i\n  Nullable %i\n",
                 ColumnName[i], (int)ColumnNameLen[i], (int)ColumnDataType[i],
                 (int)ColumnDataSize[i], (int)ColumnDataDigits[i],
                 (int)ColumnDataNullable[i]);
    }
}

CppUnit::Test* 
odbcTestSuite() {
    CppUnit::TestSuite *suite = new CppUnit::TestSuite("OdbcDriver");

    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("getDSN", &ODBCDriverTest::getDSNTest));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("connect", &ODBCDriverTest::connectionTest));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("sqlGetInfo", &ODBCDriverTest::sqlGetInfoTest));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("sqlGetTypeInfo", &ODBCDriverTest::sqlGetTypeInfoTest));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("getSysTables", &ODBCDriverTest::getSysTables));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("getTables", &ODBCDriverTest::getTables));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("runQueries", &ODBCDriverTest::runQueries));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("colAttribute", &ODBCDriverTest::colAttrTest));
    // suite->addTest(
    //     new CppUnit::TestCaller<ODBCDriverTest>("bug53242", &ODBCDriverTest::bug53242Test));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("bug55356", &ODBCDriverTest::bug55356Test));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("describeColTest", &ODBCDriverTest::describeColTest));
    suite->addTest(
        new CppUnit::TestCaller<ODBCDriverTest>("getFunctions", &ODBCDriverTest::getFunctionsTest));

    return suite;
}

void testOdbcDriver() {
    CppUnit::TextUi::TestRunner runner;
    vector<CppUnit::Test*> suites;

    suites.push_back(odbcTestSuite());

    for (int i = 0; i < suites.size(); i++) {
        runner.addTest(suites[i]);
    }

    runner.run();
}

int main() {
    testOdbcDriver();

    return 0;
}
