apr_dbd_odbc.c   [plain text]


/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "apu.h"
#if APU_HAVE_ODBC

#include "apr.h"
#include "apr_strings.h"
#include "apr_buckets.h"
#include "apr_env.h"
#include "apr_file_io.h"
#include "apr_file_info.h"
#include "apr_dbd_internal.h"
#include "apr_thread_proc.h"
#include "apu_version.h"
#include "apu_config.h"

#include <stdlib.h>

/* If library is ODBC-V2, use macros for limited ODBC-V2 support 
 * No random access in V2.
 */
#ifdef ODBCV2
#define ODBCVER 0x0200
#include "apr_dbd_odbc_v2.h"
#endif

/* standard ODBC include files */
#ifdef HAVE_SQL_H
#include <sql.h>
#include <sqlext.h>
#elif defined(HAVE_ODBC_SQL_H)
#include <odbc/sql.h>
#include <odbc/sqlext.h>
#endif

/* Driver name is "odbc" and the entry point is 'apr_dbd_odbc_driver' 
 * unless ODBC_DRIVER_NAME is defined and it is linked with another db library which
 * is ODBC source-compatible. e.g. DB2, Informix, TimesTen, mysql.  
 */
#ifndef ODBC_DRIVER_NAME
#define ODBC_DRIVER_NAME odbc
#endif
#define STRINGIFY(x) #x
#define NAMIFY2(n) apr_dbd_##n##_driver
#define NAMIFY1(n) NAMIFY2(n)
#define ODBC_DRIVER_STRING STRINGIFY(ODBC_DRIVER_NAME)
#define ODBC_DRIVER_ENTRY NAMIFY1(ODBC_DRIVER_NAME)

/* Required APR version for this driver */
#define DRIVER_APU_VERSION_MAJOR APU_MAJOR_VERSION
#define DRIVER_APU_VERSION_MINOR APU_MINOR_VERSION

static SQLHANDLE henv = NULL;           /* ODBC ENV handle is process-wide */

/* Use a CHECK_ERROR macro so we can grab the source line numbers
 * for error reports
 */
static void check_error(apr_dbd_t *a, const char *step, SQLRETURN rc,
                 SQLSMALLINT type, SQLHANDLE h, int line);
#define CHECK_ERROR(a,s,r,t,h)  check_error(a,s,r,t,h, __LINE__)

#define SOURCE_FILE __FILE__            /* source file for error messages */
#define MAX_ERROR_STRING 1024           /* max length of message in dbc */
#define MAX_COLUMN_NAME 256             /* longest column name recognized */
#define DEFAULT_BUFFER_SIZE 1024        /* value for defaultBufferSize */

#define MAX_PARAMS  20
#define DEFAULTSEPS " \t\r\n,="
#define CSINGLEQUOTE '\''
#define SSINGLEQUOTE "\'"

#define TEXTMODE 1              /* used for text (APR 1.2) mode params */
#define BINARYMODE 0            /* used for binary (APR 1.3+) mode params */

/* Identify datatypes which are LOBs 
 * - DB2 DRDA driver uses undefined types -98 and -99 for CLOB & BLOB
 */
#define IS_LOB(t)  (t == SQL_LONGVARCHAR \
     || t == SQL_LONGVARBINARY || t == SQL_VARBINARY \
     || t == -98 || t == -99)

/* These types are CLOBs 
 * - DB2 DRDA driver uses undefined type -98 for CLOB
 */
#define IS_CLOB(t) \
    (t == SQL_LONGVARCHAR || t == -98)

/* Convert a SQL result to an APR result */
#define APR_FROM_SQL_RESULT(rc) \
    (SQL_SUCCEEDED(rc) ? APR_SUCCESS : APR_EGENERAL)

/* DBD opaque structures */
struct apr_dbd_t
{
    SQLHANDLE dbc;              /* SQL connection handle - NULL after close */
    apr_pool_t *pool;           /* connection lifetime pool */
    char *dbname;               /* ODBC datasource */
    int lasterrorcode;
    int lineNumber;
    char lastError[MAX_ERROR_STRING];
    int defaultBufferSize;      /* used for CLOBs in text mode, 
                                 * and when fld size is indeterminate */
    int transaction_mode;
    int dboptions;              /* driver options re SQLGetData */
    int default_transaction_mode;
    int can_commit;             /* controls end_trans behavior */
};

struct apr_dbd_results_t
{
    SQLHANDLE stmt;             /* parent sql statement handle */
    SQLHANDLE dbc;              /* parent sql connection handle */
    apr_pool_t *pool;           /* pool from query or select */
    apr_dbd_t *apr_dbd;         /* parent DBD connection handle */
    int random;                 /* random access requested */
    int ncols;                  /* number of columns */
    int isclosed;               /* cursor has been closed */
    char **colnames;            /* array of column names (NULL until used) */
    SQLPOINTER *colptrs;        /* pointers to column data */
    SQLINTEGER *colsizes;       /* sizes for columns (enough for txt or bin) */
    SQLINTEGER *coltextsizes;   /* max-sizes if converted to text */
    SQLSMALLINT *coltypes;      /* array of SQL data types for columns */
    SQLLEN *colinds;            /* array of SQL data indicator/strlens */
    int *colstate;              /* array of column states
                                 * - avail, bound, present, unavail 
                                 */
    int *all_data_fetched;      /* flags data as all fetched, for LOBs  */
    void *data;                 /* buffer for all data for one row */
};

enum                            /* results column states */
{
    COL_AVAIL,                  /* data may be retrieved with SQLGetData */
    COL_PRESENT,                /* data has been retrieved with SQLGetData */
    COL_BOUND,                  /* column is bound to colptr */
    COL_RETRIEVED,              /* all data from column has been returned */
    COL_UNAVAIL                 /* column is unavailable because ODBC driver
                                 *  requires that columns be retrieved
                                 *  in ascending order and a higher col 
                                 *  was accessed
                                 */
};

struct apr_dbd_row_t {
    SQLHANDLE stmt;             /* parent ODBC statement handle */
    SQLHANDLE dbc;              /* parent ODBC connection handle */
    apr_pool_t *pool;           /* pool from get_row */
    apr_dbd_results_t *res;
};

struct apr_dbd_transaction_t {
    SQLHANDLE dbc;              /* parent ODBC connection handle */
    apr_dbd_t *apr_dbd;         /* parent DBD connection handle */
};

struct apr_dbd_prepared_t {
    SQLHANDLE stmt;             /* ODBC statement handle */
    SQLHANDLE dbc;              /* parent ODBC connection handle */
    apr_dbd_t *apr_dbd;
    int nargs;
    int nvals;
    int *types;                 /* array of DBD data types */
};

static void odbc_lob_bucket_destroy(void *data);
static apr_status_t odbc_lob_bucket_setaside(apr_bucket *e, apr_pool_t *pool);
static apr_status_t odbc_lob_bucket_read(apr_bucket *e, const char **str,
                                         apr_size_t *len, apr_read_type_e block);

/* the ODBC LOB bucket type */
static const apr_bucket_type_t odbc_bucket_type = {
    "ODBC_LOB", 5, APR_BUCKET_DATA,
    odbc_lob_bucket_destroy,
    odbc_lob_bucket_read,
    odbc_lob_bucket_setaside,
    apr_bucket_shared_split,
    apr_bucket_shared_copy
};

/* ODBC LOB bucket data */
typedef struct {
    /** Ref count for shared bucket */
    apr_bucket_refcount  refcount;
    const apr_dbd_row_t *row;
    int col;
    SQLSMALLINT type;
} odbc_bucket;

/* SQL datatype mappings to DBD datatypes 
 * These tables must correspond *exactly* to the apr_dbd_type_e enum 
 * in apr_dbd.h
 */

/* ODBC "C" types to DBD datatypes  */
static SQLSMALLINT const sqlCtype[] = {
    SQL_C_DEFAULT,                  /* APR_DBD_TYPE_NONE              */
    SQL_C_STINYINT,                 /* APR_DBD_TYPE_TINY,       \%hhd */
    SQL_C_UTINYINT,                 /* APR_DBD_TYPE_UTINY,      \%hhu */
    SQL_C_SSHORT,                   /* APR_DBD_TYPE_SHORT,      \%hd  */
    SQL_C_USHORT,                   /* APR_DBD_TYPE_USHORT,     \%hu  */
    SQL_C_SLONG,                    /* APR_DBD_TYPE_INT,        \%d   */
    SQL_C_ULONG,                    /* APR_DBD_TYPE_UINT,       \%u   */
    SQL_C_SLONG,                    /* APR_DBD_TYPE_LONG,       \%ld  */
    SQL_C_ULONG,                    /* APR_DBD_TYPE_ULONG,      \%lu  */
    SQL_C_SBIGINT,                  /* APR_DBD_TYPE_LONGLONG,   \%lld */
    SQL_C_UBIGINT,                  /* APR_DBD_TYPE_ULONGLONG,  \%llu */
    SQL_C_FLOAT,                    /* APR_DBD_TYPE_FLOAT,      \%f   */
    SQL_C_DOUBLE,                   /* APR_DBD_TYPE_DOUBLE,     \%lf  */
    SQL_C_CHAR,                     /* APR_DBD_TYPE_STRING,     \%s   */
    SQL_C_CHAR,                     /* APR_DBD_TYPE_TEXT,       \%pDt */
    SQL_C_CHAR, /*SQL_C_TYPE_TIME,      APR_DBD_TYPE_TIME,       \%pDi */
    SQL_C_CHAR, /*SQL_C_TYPE_DATE,      APR_DBD_TYPE_DATE,       \%pDd */
    SQL_C_CHAR, /*SQL_C_TYPE_TIMESTAMP, APR_DBD_TYPE_DATETIME,   \%pDa */
    SQL_C_CHAR, /*SQL_C_TYPE_TIMESTAMP, APR_DBD_TYPE_TIMESTAMP,  \%pDs */
    SQL_C_CHAR, /*SQL_C_TYPE_TIMESTAMP, APR_DBD_TYPE_ZTIMESTAMP, \%pDz */
    SQL_LONGVARBINARY,              /* APR_DBD_TYPE_BLOB,       \%pDb */
    SQL_LONGVARCHAR,                /* APR_DBD_TYPE_CLOB,       \%pDc */
    SQL_TYPE_NULL                   /* APR_DBD_TYPE_NULL        \%pDn */
};
#define NUM_APR_DBD_TYPES (sizeof(sqlCtype) / sizeof(sqlCtype[0]))

/*  ODBC Base types to DBD datatypes */
static SQLSMALLINT const sqlBaseType[] = {
    SQL_C_DEFAULT,              /* APR_DBD_TYPE_NONE              */
    SQL_TINYINT,                /* APR_DBD_TYPE_TINY,       \%hhd */
    SQL_TINYINT,                /* APR_DBD_TYPE_UTINY,      \%hhu */
    SQL_SMALLINT,               /* APR_DBD_TYPE_SHORT,      \%hd  */
    SQL_SMALLINT,               /* APR_DBD_TYPE_USHORT,     \%hu  */
    SQL_INTEGER,                /* APR_DBD_TYPE_INT,        \%d   */
    SQL_INTEGER,                /* APR_DBD_TYPE_UINT,       \%u   */
    SQL_INTEGER,                /* APR_DBD_TYPE_LONG,       \%ld  */
    SQL_INTEGER,                /* APR_DBD_TYPE_ULONG,      \%lu  */
    SQL_BIGINT,                 /* APR_DBD_TYPE_LONGLONG,   \%lld */
    SQL_BIGINT,                 /* APR_DBD_TYPE_ULONGLONG,  \%llu */
    SQL_FLOAT,                  /* APR_DBD_TYPE_FLOAT,      \%f   */
    SQL_DOUBLE,                 /* APR_DBD_TYPE_DOUBLE,     \%lf  */
    SQL_CHAR,                   /* APR_DBD_TYPE_STRING,     \%s   */
    SQL_CHAR,                   /* APR_DBD_TYPE_TEXT,       \%pDt */
    SQL_CHAR, /*SQL_TIME,          APR_DBD_TYPE_TIME,       \%pDi */
    SQL_CHAR, /*SQL_DATE,          APR_DBD_TYPE_DATE,       \%pDd */
    SQL_CHAR, /*SQL_TIMESTAMP,     APR_DBD_TYPE_DATETIME,   \%pDa */
    SQL_CHAR, /*SQL_TIMESTAMP,     APR_DBD_TYPE_TIMESTAMP,  \%pDs */
    SQL_CHAR, /*SQL_TIMESTAMP,     APR_DBD_TYPE_ZTIMESTAMP, \%pDz */
    SQL_LONGVARBINARY,          /* APR_DBD_TYPE_BLOB,       \%pDb */
    SQL_LONGVARCHAR,            /* APR_DBD_TYPE_CLOB,       \%pDc */
    SQL_TYPE_NULL               /* APR_DBD_TYPE_NULL        \%pDn */
};

/*  result sizes for DBD datatypes (-1 for null-terminated) */
static int const sqlSizes[] = {
    0,
    sizeof(char),               /**< \%hhd out: char* */
    sizeof(unsigned char),      /**< \%hhu out: unsigned char* */
    sizeof(short),              /**< \%hd  out: short* */
    sizeof(unsigned short),     /**< \%hu  out: unsigned short* */
    sizeof(int),                /**< \%d   out: int* */
    sizeof(unsigned int),       /**< \%u   out: unsigned int* */
    sizeof(long),               /**< \%ld  out: long* */
    sizeof(unsigned long),      /**< \%lu  out: unsigned long* */
    sizeof(apr_int64_t),        /**< \%lld out: apr_int64_t* */
    sizeof(apr_uint64_t),       /**< \%llu out: apr_uint64_t* */
    sizeof(float),              /**< \%f   out: float* */
    sizeof(double),             /**< \%lf  out: double* */
    -1,                         /**< \%s   out: char** */
    -1,                         /**< \%pDt out: char** */
    -1,                         /**< \%pDi out: char** */
    -1,                         /**< \%pDd out: char** */
    -1,                         /**< \%pDa out: char** */
    -1,                         /**< \%pDs out: char** */
    -1,                         /**< \%pDz out: char** */
    sizeof(apr_bucket_brigade), /**< \%pDb out: apr_bucket_brigade* */
    sizeof(apr_bucket_brigade), /**< \%pDc out: apr_bucket_brigade* */
    0                           /**< \%pDn : in: void*, out: void** */
};

/*
 * local functions
 */

/* close any open results for the connection */
static apr_status_t odbc_close_results(void *d)
{
    apr_dbd_results_t *dbr = (apr_dbd_results_t *)d;
    SQLRETURN rc = SQL_SUCCESS;
    
    if (dbr && dbr->apr_dbd && dbr->apr_dbd->dbc) {
    	if (!dbr->isclosed)
            rc = SQLCloseCursor(dbr->stmt);
    	dbr->isclosed = 1;
    }
    return APR_FROM_SQL_RESULT(rc);
}

/* close the ODBC statement handle from a  prepare */
static apr_status_t odbc_close_pstmt(void *s)
{   
    SQLRETURN rc = APR_SUCCESS;
    apr_dbd_prepared_t *statement = s;

    /* stmt is closed if connection has already been closed */
    if (statement) {
        SQLHANDLE hstmt = statement->stmt;

        if (hstmt && statement->apr_dbd && statement->apr_dbd->dbc) {
            rc = SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
        }
        statement->stmt = NULL;
    }
    return APR_FROM_SQL_RESULT(rc);
}

/* close: close/release a connection obtained from open() */
static apr_status_t odbc_close(apr_dbd_t *handle)
{
    SQLRETURN rc = SQL_SUCCESS;

    if (handle->dbc) {
        rc = SQLDisconnect(handle->dbc);
        CHECK_ERROR(handle, "SQLDisconnect", rc, SQL_HANDLE_DBC, handle->dbc);
        rc = SQLFreeHandle(SQL_HANDLE_DBC, handle->dbc);
        CHECK_ERROR(handle, "SQLFreeHandle (DBC)", rc, SQL_HANDLE_ENV, henv);
        handle->dbc = NULL;
    }
    return APR_FROM_SQL_RESULT(rc);
}

/* odbc_close re-defined for passing to pool cleanup */
static apr_status_t odbc_close_cleanup(void *handle)
{
    return odbc_close((apr_dbd_t *)handle);
}

/* close the ODBC environment handle at process termination */
static apr_status_t odbc_close_env(SQLHANDLE henv)
{   
    SQLRETURN rc;

    rc = SQLFreeHandle(SQL_HANDLE_ENV, henv);
    henv = NULL;
    return APR_FROM_SQL_RESULT(rc);
}

/* setup the arrays in results for all the returned columns */
static SQLRETURN odbc_set_result_column(int icol, apr_dbd_results_t *res, 
                                        SQLHANDLE stmt)
{
    SQLRETURN rc;
    int maxsize, textsize, realsize, type, isunsigned = 1;

    /* discover the sql type */
    rc = SQLColAttribute(stmt, icol + 1, SQL_DESC_UNSIGNED, NULL, 0, NULL,
                         (SQLPOINTER)&isunsigned);
    isunsigned = (isunsigned == SQL_TRUE);

    rc = SQLColAttribute(stmt, icol + 1, SQL_DESC_TYPE, NULL, 0, NULL,
                         (SQLPOINTER)&type);
    if (!SQL_SUCCEEDED(rc) || type == SQL_UNKNOWN_TYPE) {
        /* MANY ODBC v2 datasources only supply CONCISE_TYPE */
        rc = SQLColAttribute(stmt, icol + 1, SQL_DESC_CONCISE_TYPE, NULL,
                             0, NULL, (SQLPOINTER)&type);
    }

    if (!SQL_SUCCEEDED(rc)) {
        /* if still unknown make it CHAR */
        type = SQL_C_CHAR;
    }

    switch (type) {
    case SQL_INTEGER:
    case SQL_SMALLINT:
    case SQL_TINYINT:
    case SQL_BIGINT:
      /* fix these numeric binary types up as signed/unsigned for C types */
      type += (isunsigned) ? SQL_UNSIGNED_OFFSET : SQL_SIGNED_OFFSET;
      break;
    /* LOB types are not changed to C types */
    case SQL_LONGVARCHAR: 
        type = SQL_LONGVARCHAR; 
        break;
    case SQL_LONGVARBINARY: 
        type = SQL_LONGVARBINARY; 
        break;
    case SQL_FLOAT : 
        type = SQL_C_FLOAT; 
        break;
    case SQL_DOUBLE : 
        type = SQL_C_DOUBLE; 
        break;

    /* DBD wants times as strings */
    case SQL_TIMESTAMP:      
    case SQL_DATE:
    case SQL_TIME:
    default:
      type = SQL_C_CHAR;
    }

    res->coltypes[icol] = type;

    /* size if retrieved as text */
    rc = SQLColAttribute(stmt, icol + 1, SQL_DESC_DISPLAY_SIZE, NULL, 0,
                         NULL, (SQLPOINTER)&textsize);
    if (!SQL_SUCCEEDED(rc) || textsize < 0) {
        textsize = res->apr_dbd->defaultBufferSize;
    }
    /* for null-term, which sometimes isn't included */
    textsize++;

    /* real size */
    rc = SQLColAttribute(stmt, icol + 1, SQL_DESC_OCTET_LENGTH, NULL, 0,
                         NULL, (SQLPOINTER)&realsize);
    if (!SQL_SUCCEEDED(rc)) {
        realsize = textsize;
    }

    maxsize = (textsize > realsize) ? textsize : realsize;
    if (IS_LOB(type) || maxsize <= 0) {
        /* LOB types are never bound and have a NULL colptr for binary.
         * Ingore their real (1-2gb) length & use a default - the larger
         * of defaultBufferSize or APR_BUCKET_BUFF_SIZE.
         * If not a LOB, but simply unknown length - always use defaultBufferSize.
         */
        maxsize = res->apr_dbd->defaultBufferSize;
        if (IS_LOB(type) && maxsize < APR_BUCKET_BUFF_SIZE) {
            maxsize = APR_BUCKET_BUFF_SIZE;
        }

        res->colptrs[icol] =  NULL;
        res->colstate[icol] = COL_AVAIL;
        res->colsizes[icol] = maxsize;
        rc = SQL_SUCCESS;
    }
    else {
        res->colptrs[icol] = apr_pcalloc(res->pool, maxsize);
        res->colsizes[icol] = maxsize;
        if (res->apr_dbd->dboptions & SQL_GD_BOUND) {
            /* we are allowed to call SQLGetData if we need to */
            rc = SQLBindCol(stmt, icol + 1, res->coltypes[icol], 
                            res->colptrs[icol], maxsize, 
                            &(res->colinds[icol]));
            CHECK_ERROR(res->apr_dbd, "SQLBindCol", rc, SQL_HANDLE_STMT,
                        stmt);
            res->colstate[icol] = SQL_SUCCEEDED(rc) ? COL_BOUND : COL_AVAIL;
        }
        else {
            /* this driver won't allow us to call SQLGetData on bound 
             * columns - so don't bind any
             */
            res->colstate[icol] = COL_AVAIL;
            rc = SQL_SUCCESS;
        }
    }
    return rc;
}

/* create and populate an apr_dbd_results_t for a select */
static SQLRETURN odbc_create_results(apr_dbd_t *handle, SQLHANDLE hstmt,
                                     apr_pool_t *pool, const int random,
                                     apr_dbd_results_t **res)
{
    SQLRETURN rc;
    SQLSMALLINT ncols;

    *res = apr_pcalloc(pool, sizeof(apr_dbd_results_t));
    (*res)->stmt = hstmt;
    (*res)->dbc = handle->dbc;
    (*res)->pool = pool;
    (*res)->random = random;
    (*res)->apr_dbd = handle;
    rc = SQLNumResultCols(hstmt, &ncols);
    CHECK_ERROR(handle, "SQLNumResultCols", rc, SQL_HANDLE_STMT, hstmt);
    (*res)->ncols = ncols;

    if (SQL_SUCCEEDED(rc)) {
        int i;

        (*res)->colnames = apr_pcalloc(pool, ncols * sizeof(char *));
        (*res)->colptrs = apr_pcalloc(pool, ncols * sizeof(void *));
        (*res)->colsizes = apr_pcalloc(pool, ncols * sizeof(SQLINTEGER));
        (*res)->coltypes = apr_pcalloc(pool, ncols * sizeof(SQLSMALLINT));
        (*res)->colinds = apr_pcalloc(pool, ncols * sizeof(SQLLEN));
        (*res)->colstate = apr_pcalloc(pool, ncols * sizeof(int));
        (*res)->ncols = ncols;

        for (i = 0; i < ncols; i++) {
            odbc_set_result_column(i, (*res), hstmt);
        }
    }
    return rc;
}


/* bind a parameter - input params only, does not support output parameters */
static SQLRETURN odbc_bind_param(apr_pool_t *pool,
                                 apr_dbd_prepared_t *statement, const int narg,
                                 const SQLSMALLINT type, int *argp,
                                 const void **args, const int textmode)
{
    SQLRETURN rc;
    SQLSMALLINT baseType, cType;
    void *ptr;
    SQLULEN len;
    SQLLEN *indicator;
    static SQLLEN nullValue = SQL_NULL_DATA;
    static SQLSMALLINT inOut = SQL_PARAM_INPUT;     /* only input params */

    /* bind a NULL data value */
    if (args[*argp] == NULL || type == APR_DBD_TYPE_NULL) {
        baseType = SQL_CHAR;
        cType = SQL_C_CHAR;
        ptr = &nullValue;
        len = sizeof(SQLINTEGER);
        indicator = &nullValue;
        (*argp)++;
    }
    /* bind a non-NULL data value */
    else {
        if (type < 0 || type >= NUM_APR_DBD_TYPES) {
            return APR_EGENERAL;
        }

        baseType = sqlBaseType[type];
        cType = sqlCtype[type];
        indicator = NULL;
        /* LOBs */
        if (IS_LOB(cType)) {
            ptr = (void *)args[*argp];
            len = (SQLULEN) * (apr_size_t *)args[*argp + 1];
            cType = (IS_CLOB(cType)) ? SQL_C_CHAR : SQL_C_DEFAULT;
            (*argp) += 4;  /* LOBs consume 4 args (last two are unused) */
        }
        /* non-LOBs */
        else {
            switch (baseType) {
            case SQL_CHAR:
            case SQL_DATE:
            case SQL_TIME:
            case SQL_TIMESTAMP:
                ptr = (void *)args[*argp];
                len = (SQLULEN)strlen(ptr);
                break;
            case SQL_TINYINT:
                ptr = apr_palloc(pool, sizeof(unsigned char));
                len = sizeof(unsigned char);
                *(unsigned char *)ptr =
                    (textmode ?
                     atoi(args[*argp]) : *(unsigned char *)args[*argp]);
                break;
            case SQL_SMALLINT:
                ptr = apr_palloc(pool, sizeof(short));
                len = sizeof(short);
                *(short *)ptr =
                    (textmode ? atoi(args[*argp]) : *(short *)args[*argp]);
                break;
            case SQL_INTEGER:
                ptr = apr_palloc(pool, sizeof(int));
                len = sizeof(int);
                *(long *)ptr =
                    (textmode ? atol(args[*argp]) : *(long *)args[*argp]);
                break;
            case SQL_FLOAT:
                ptr = apr_palloc(pool, sizeof(float));
                len = sizeof(float);
                *(float *)ptr =
                    (textmode ?
                     (float)atof(args[*argp]) : *(float *)args[*argp]);
                break;
            case SQL_DOUBLE:
                ptr = apr_palloc(pool, sizeof(double));
                len = sizeof(double);
                *(double *)ptr =
                    (textmode ? atof(args[*argp]) : *(double *)
                     args[*argp]);
                break;
            case SQL_BIGINT:
                ptr = apr_palloc(pool, sizeof(apr_int64_t));
                len = sizeof(apr_int64_t);
                *(apr_int64_t *)ptr =
                    (textmode ?
                     apr_atoi64(args[*argp]) : *(apr_int64_t *)args[*argp]);
                break;
            default:
                return APR_EGENERAL;
            }
            (*argp)++;          /* non LOBs consume one argument */
        }
    }
    rc = SQLBindParameter(statement->stmt, narg, inOut, cType, 
                          baseType, len, 0, ptr, len, indicator);
    CHECK_ERROR(statement->apr_dbd, "SQLBindParameter", rc, SQL_HANDLE_STMT,
                statement->stmt);
    return rc;
}

/* LOB / Bucket Brigade functions */

/* bucket type specific destroy */
static void odbc_lob_bucket_destroy(void *data)
{
    odbc_bucket *bd = data;

    if (apr_bucket_shared_destroy(bd))
        apr_bucket_free(bd);
}

/* set aside a bucket if possible */
static apr_status_t odbc_lob_bucket_setaside(apr_bucket *e, apr_pool_t *pool)
{
    odbc_bucket *bd = (odbc_bucket *)e->data;

    /* Unlikely - but if the row pool is ancestor of this pool then it is OK */
    if (apr_pool_is_ancestor(bd->row->pool, pool))
        return APR_SUCCESS;
    
    return apr_bucket_setaside_notimpl(e, pool);
}

/* split a bucket into a heap bucket followed by a LOB bkt w/remaining data */
static apr_status_t odbc_lob_bucket_read(apr_bucket *e, const char **str,
                                         apr_size_t *len, apr_read_type_e block)
{
    SQLRETURN rc;
    SQLLEN len_indicator;
    SQLSMALLINT type;
    odbc_bucket *bd = (odbc_bucket *)e->data;
    apr_bucket *nxt;
    void *buf;
    int bufsize = bd->row->res->apr_dbd->defaultBufferSize;
    int eos;
    
    /* C type is CHAR for CLOBs, DEFAULT for BLOBs */
    type = bd->row->res->coltypes[bd->col];
    type = (type == SQL_LONGVARCHAR) ? SQL_C_CHAR : SQL_C_DEFAULT;

    /* LOB buffers are always at least APR_BUCKET_BUFF_SIZE, 
     *   but they may be much bigger per the BUFSIZE parameter.
     */
    if (bufsize < APR_BUCKET_BUFF_SIZE)
        bufsize = APR_BUCKET_BUFF_SIZE;

    buf = apr_bucket_alloc(bufsize, e->list);
    *str = NULL;
    *len = 0;

    rc = SQLGetData(bd->row->res->stmt, bd->col + 1, 
                    type, buf, bufsize, 
                    &len_indicator);

    CHECK_ERROR(bd->row->res->apr_dbd, "SQLGetData", rc, 
                SQL_HANDLE_STMT, bd->row->res->stmt);
    
    if (rc == SQL_NO_DATA || len_indicator == SQL_NULL_DATA || len_indicator < 0)
        len_indicator = 0;

    if (SQL_SUCCEEDED(rc) || rc == SQL_NO_DATA) {

        if (rc == SQL_SUCCESS_WITH_INFO
            && (len_indicator == SQL_NO_TOTAL || len_indicator >= bufsize)) {
            /* not the last read = a full buffer. CLOBs have a null terminator */
            *len = bufsize - (IS_CLOB(bd->type) ? 1 : 0 );

             eos = 0;
        }
        else {
            /* the last read - len_indicator is supposed to be the length, 
             * but some driver get this wrong and return the total length.
             * We try to handle both interpretations.
             */
            *len =  (len_indicator > bufsize 
                     && len_indicator >= (SQLLEN)e->start)
                ? (len_indicator - (SQLLEN)e->start) : len_indicator;

            eos = 1;
        }

        if (!eos) {
            /* Create a new LOB bucket to append and append it */
            nxt = apr_bucket_alloc(sizeof(apr_bucket *), e->list);
            APR_BUCKET_INIT(nxt);
            nxt->length = -1;
            nxt->data   = e->data;
            nxt->type   = &odbc_bucket_type;
            nxt->free   = apr_bucket_free;
            nxt->list   = e->list;
            nxt->start  = e->start + *len;
            APR_BUCKET_INSERT_AFTER(e, nxt);
        }
        else {
            odbc_lob_bucket_destroy(e->data);
        }
        /* make current bucket into a heap bucket */
        apr_bucket_heap_make(e, buf, *len, apr_bucket_free);
        *str = buf;

        /* No data is success in this context */
        rc = SQL_SUCCESS;
    }
    return APR_FROM_SQL_RESULT(rc);
}

/* Create a bucket brigade on the row pool for a LOB column */
static apr_status_t odbc_create_bucket(const apr_dbd_row_t *row, const int col, 
                                       SQLSMALLINT type, apr_bucket_brigade *bb)
{
    apr_bucket_alloc_t *list = bb->bucket_alloc;
    apr_bucket *b = apr_bucket_alloc(sizeof(*b), list);
    odbc_bucket *bd = apr_bucket_alloc(sizeof(odbc_bucket), list);
    apr_bucket *eos = apr_bucket_eos_create(list);
    
    bd->row = row;
    bd->col = col;
    bd->type = type;

    APR_BUCKET_INIT(b);
    b->type = &odbc_bucket_type;
    b->free = apr_bucket_free;
    b->list = list;
    /* LOB lengths are unknown in ODBC */
    b = apr_bucket_shared_make(b, bd, 0, -1);

    APR_BRIGADE_INSERT_TAIL(bb, b);
    APR_BRIGADE_INSERT_TAIL(bb, eos);

    return APR_SUCCESS;
}

/* returns a data pointer for a column,  returns NULL for NULL value,
 * return -1 if data not available
 */
static void *odbc_get(const apr_dbd_row_t *row, const int col, 
                      const SQLSMALLINT sqltype)
{
    SQLRETURN rc;
    SQLLEN indicator;
    int state = row->res->colstate[col];
    int options = row->res->apr_dbd->dboptions;

    switch (state) {
    case (COL_UNAVAIL):
        return (void *)-1;
    case (COL_RETRIEVED):
        return NULL;

    case (COL_BOUND):
    case (COL_PRESENT): 
        if (sqltype == row->res->coltypes[col]) {
            /* same type and we already have the data */
            row->res->colstate[col] = COL_RETRIEVED;
            return (row->res->colinds[col] == SQL_NULL_DATA) ? 
                NULL : row->res->colptrs[col];
        }
    }

    /* we need to get the data now */
    if (!(options & SQL_GD_ANY_ORDER)) {
        /* this ODBC driver requires columns to be retrieved in order,
         * so we attempt to get every prior un-gotten non-LOB column
         */
        int i;
        for (i = 0; i < col; i++) {
            if (row->res->colstate[i] == COL_AVAIL) {
                if (IS_LOB(row->res->coltypes[i]))
                       row->res->colstate[i] = COL_UNAVAIL;
                else {
                    odbc_get(row, i, row->res->coltypes[i]);
                    row->res->colstate[i] = COL_PRESENT;
                }
            }
        }
    }

    if ((state == COL_BOUND && !(options & SQL_GD_BOUND)))
        /* this driver won't let us re-get bound columns */
        return (void *)-1;

    /* a LOB might not have a buffer allocated yet - so create one */
    if (!row->res->colptrs[col])
        row->res->colptrs[col] = apr_pcalloc(row->pool, row->res->colsizes[col]);

    rc = SQLGetData(row->res->stmt, col + 1, sqltype, row->res->colptrs[col],
                    row->res->colsizes[col], &indicator);
    CHECK_ERROR(row->res->apr_dbd, "SQLGetData", rc, SQL_HANDLE_STMT, 
                row->res->stmt);
    if (indicator == SQL_NULL_DATA || rc == SQL_NO_DATA)
        return NULL;

    if (SQL_SUCCEEDED(rc)) {
        /* whatever it was originally, it is now this sqltype */
        row->res->coltypes[col] = sqltype;
        /* this allows getting CLOBs in text mode by calling get_entry
         *   until it returns NULL
         */
        row->res->colstate[col] = 
            (rc == SQL_SUCCESS_WITH_INFO) ? COL_AVAIL : COL_RETRIEVED;
        return row->res->colptrs[col];
    }
    else
        return (void *)-1;
}

/* Parse the parameter string for open */
static apr_status_t odbc_parse_params(apr_pool_t *pool, const char *params,
                               int *connect, SQLCHAR **datasource, 
                               SQLCHAR **user, SQLCHAR **password, 
                               int *defaultBufferSize, int *nattrs,
                               int **attrs, int **attrvals)
{
    char *seps, *last, *next, *name[MAX_PARAMS], *val[MAX_PARAMS];
    int nparams = 0, i, j;

    *attrs = apr_pcalloc(pool, MAX_PARAMS * sizeof(char *));
    *attrvals = apr_pcalloc(pool, MAX_PARAMS * sizeof(int));
    *nattrs = 0;
    seps = DEFAULTSEPS;
    name[nparams] = apr_strtok(apr_pstrdup(pool, params), seps, &last);

    /* no params is OK here - let connect return a more useful error msg */
    if (!name[nparams])
        return SQL_SUCCESS;

    do {
        if (last[strspn(last, seps)] == CSINGLEQUOTE) {
            last += strspn(last, seps);
            seps=SSINGLEQUOTE;
        }
        val[nparams] = apr_strtok(NULL, seps, &last);
        seps = DEFAULTSEPS;

        ++nparams;
        next = apr_strtok(NULL, seps, &last);
        if (!next) {
            break;
        }
        if (nparams >= MAX_PARAMS) {
            /* too many parameters, no place to store */
            return APR_EGENERAL;
        }
        name[nparams] = next;
    } while (1);

    for (j = i = 0; i < nparams; i++) {
        if (!apr_strnatcasecmp(name[i], "CONNECT")) {
            *datasource = (SQLCHAR *)apr_pstrdup(pool, val[i]);
            *connect = 1;
        }
        else if (!apr_strnatcasecmp(name[i], "DATASOURCE")) {
            *datasource = (SQLCHAR *)apr_pstrdup(pool, val[i]);
            *connect = 0;
        }
        else if (!apr_strnatcasecmp(name[i], "USER")) {
            *user = (SQLCHAR *)apr_pstrdup(pool, val[i]);
        }
        else if (!apr_strnatcasecmp(name[i], "PASSWORD")) {
            *password = (SQLCHAR *)apr_pstrdup(pool, val[i]);
        }
        else if (!apr_strnatcasecmp(name[i], "BUFSIZE")) {
            *defaultBufferSize = atoi(val[i]);
        }
        else if (!apr_strnatcasecmp(name[i], "ACCESS")) {
            if (!apr_strnatcasecmp(val[i], "READ_ONLY"))
                (*attrvals)[j] = SQL_MODE_READ_ONLY;
            else if (!apr_strnatcasecmp(val[i], "READ_WRITE"))
                (*attrvals)[j] = SQL_MODE_READ_WRITE;
            else
                return SQL_ERROR;
            (*attrs)[j++] = SQL_ATTR_ACCESS_MODE;
        }
        else if (!apr_strnatcasecmp(name[i], "CTIMEOUT")) {
            (*attrvals)[j] = atoi(val[i]);
            (*attrs)[j++] = SQL_ATTR_LOGIN_TIMEOUT;
        }
        else if (!apr_strnatcasecmp(name[i], "STIMEOUT")) {
            (*attrvals)[j] = atoi(val[i]);
            (*attrs)[j++] = SQL_ATTR_CONNECTION_TIMEOUT;
        }
        else if (!apr_strnatcasecmp(name[i], "TXMODE")) {
            if (!apr_strnatcasecmp(val[i], "READ_UNCOMMITTED"))
                (*attrvals)[j] = SQL_TXN_READ_UNCOMMITTED;
            else if (!apr_strnatcasecmp(val[i], "READ_COMMITTED"))
                (*attrvals)[j] = SQL_TXN_READ_COMMITTED;
            else if (!apr_strnatcasecmp(val[i], "REPEATABLE_READ"))
                (*attrvals)[j] = SQL_TXN_REPEATABLE_READ;
            else if (!apr_strnatcasecmp(val[i], "SERIALIZABLE"))
                (*attrvals)[j] = SQL_TXN_SERIALIZABLE;
            else if (!apr_strnatcasecmp(val[i], "DEFAULT"))
                continue;
            else
                return SQL_ERROR;
            (*attrs)[j++] = SQL_ATTR_TXN_ISOLATION;
        }
        else
            return SQL_ERROR;
    }
    *nattrs = j;
    return (*datasource && *defaultBufferSize) ? APR_SUCCESS : SQL_ERROR;
}

/* common handling after ODBC calls - save error info (code and text) in dbc */
static void check_error(apr_dbd_t *dbc, const char *step, SQLRETURN rc,
                 SQLSMALLINT type, SQLHANDLE h, int line)
{
    SQLCHAR buffer[512];
    SQLCHAR sqlstate[128];
    SQLINTEGER native;
    SQLSMALLINT reslength;
    char *res, *p, *end, *logval = NULL;
    int i;

    /* set info about last error in dbc  - fast return for SQL_SUCCESS  */
    if (rc == SQL_SUCCESS) {
        char successMsg[] = "[dbd_odbc] SQL_SUCCESS ";
        apr_size_t successMsgLen = sizeof successMsg - 1;

        dbc->lasterrorcode = SQL_SUCCESS;
        apr_cpystrn(dbc->lastError, successMsg, sizeof dbc->lastError);
        apr_cpystrn(dbc->lastError + successMsgLen, step,
                    sizeof dbc->lastError - successMsgLen);
        return;
    }
    switch (rc) {
    case SQL_INVALID_HANDLE:
        res = "SQL_INVALID_HANDLE";
        break;
    case SQL_ERROR:
        res = "SQL_ERROR";
        break;
    case SQL_SUCCESS_WITH_INFO:
        res = "SQL_SUCCESS_WITH_INFO";
        break;
    case SQL_STILL_EXECUTING:
        res = "SQL_STILL_EXECUTING";
        break;
    case SQL_NEED_DATA:
        res = "SQL_NEED_DATA";
        break;
    case SQL_NO_DATA:
        res = "SQL_NO_DATA";
        break;
    default:
        res = "unrecognized SQL return code";
    }
    /* these two returns are expected during normal execution */
    if (rc != SQL_SUCCESS_WITH_INFO && rc != SQL_NO_DATA 
        && dbc->can_commit != APR_DBD_TRANSACTION_IGNORE_ERRORS) {
        dbc->can_commit = APR_DBD_TRANSACTION_ROLLBACK;
    }
    p = dbc->lastError;
    end = p + sizeof(dbc->lastError);
    dbc->lasterrorcode = rc;
    p += sprintf(p, "[dbd_odbc] %.64s returned %.30s (%d) at %.24s:%d ",
                 step, res, rc, SOURCE_FILE, line - 1);
    for (i = 1, rc = 0; rc == 0; i++) {
        rc = SQLGetDiagRec(type, h, i, sqlstate, &native, buffer, 
                            sizeof(buffer), &reslength);
        if (SQL_SUCCEEDED(rc) && (p < (end - 280))) 
            p += sprintf(p, "%.256s %.20s ", buffer, sqlstate);
    }
    apr_env_get(&logval, "apr_dbd_odbc_log", dbc->pool);
    /* if env var was set or call was init/open (no dbname) - log to stderr */
    if (logval || !dbc->dbname ) {
        char timestamp[APR_CTIME_LEN];

        apr_file_t *se;
        apr_ctime(timestamp, apr_time_now());
        apr_file_open_stderr(&se, dbc->pool);
        apr_file_printf(se, "[%s] %s\n", timestamp, dbc->lastError);
    }
}

static APR_INLINE int odbc_check_rollback(apr_dbd_t *handle)
{
    if (handle->can_commit == APR_DBD_TRANSACTION_ROLLBACK) {
        handle->lasterrorcode = SQL_ERROR;
        apr_cpystrn(handle->lastError, "[dbd_odbc] Rollback pending ",
                    sizeof handle->lastError);
        return 1;
    }
    return 0;
}

/*
 *   public functions per DBD driver API
 */

/** init: allow driver to perform once-only initialisation. **/
static void odbc_init(apr_pool_t *pool)
{
    SQLRETURN rc;
    char *step;
    apr_version_t apuver;
    
    apu_version(&apuver);
    if (apuver.major != DRIVER_APU_VERSION_MAJOR 
        || apuver.minor != DRIVER_APU_VERSION_MINOR) {
            apr_file_t *se;

            apr_file_open_stderr(&se, pool);
            apr_file_printf(se, "Incorrect " ODBC_DRIVER_STRING " dbd driver version\n"
                "Attempt to load APU version %d.%d driver with APU version %d.%d\n",
                DRIVER_APU_VERSION_MAJOR, DRIVER_APU_VERSION_MINOR, 
                apuver.major, apuver.minor);
        abort();
    }

    if (henv) 
        return;

    step = "SQLAllocHandle (SQL_HANDLE_ENV)";
    rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
    apr_pool_cleanup_register(pool, henv, odbc_close_env, apr_pool_cleanup_null);
    if (SQL_SUCCEEDED(rc)) {
        step = "SQLSetEnvAttr";
        rc = SQLSetEnvAttr(henv,SQL_ATTR_ODBC_VERSION,
                          (SQLPOINTER)SQL_OV_ODBC3, 0);
    }
    else {
        apr_dbd_t tmp_dbc;
        SQLHANDLE err_h = henv;

        tmp_dbc.pool = pool;
        tmp_dbc.dbname = NULL;
        CHECK_ERROR(&tmp_dbc, step, rc, SQL_HANDLE_ENV, err_h);
    }
}

/** native_handle: return the native database handle of the underlying db **/
static void *odbc_native_handle(apr_dbd_t *handle)
{
    return handle->dbc;
}

/** open: obtain a database connection from the server rec. **/

/* It would be more efficient to allocate a single statement handle 
 * here - but SQL_ATTR_CURSOR_SCROLLABLE must be set before
 * SQLPrepare, and we don't know whether random-access is
 * specified until SQLExecute so we cannot.
 */

static apr_dbd_t *odbc_open(apr_pool_t *pool, const char *params, const char **error)
{
    SQLRETURN rc;
    SQLHANDLE hdbc = NULL;
    apr_dbd_t *handle;
    char *err_step;
    int err_htype, i;
    int defaultBufferSize = DEFAULT_BUFFER_SIZE;
    SQLHANDLE err_h = NULL;
    SQLCHAR  *datasource = (SQLCHAR *)"", *user = (SQLCHAR *)"",
             *password = (SQLCHAR *)"";
    int nattrs = 0, *attrs = NULL, *attrvals = NULL, connect = 0;

    err_step = "SQLAllocHandle (SQL_HANDLE_DBC)";
    err_htype = SQL_HANDLE_ENV;
    err_h = henv;
    rc = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    if (SQL_SUCCEEDED(rc)) {
        err_step = "Invalid DBD Parameters - open";
        err_htype = SQL_HANDLE_DBC;
        err_h = hdbc;
        rc = odbc_parse_params(pool, params, &connect, &datasource, &user,
                               &password, &defaultBufferSize, &nattrs, &attrs,
                               &attrvals);
    }
    if (SQL_SUCCEEDED(rc)) {
        for (i = 0; i < nattrs && SQL_SUCCEEDED(rc); i++) {
            err_step = "SQLSetConnectAttr (from DBD Parameters)";
            err_htype = SQL_HANDLE_DBC;
            err_h = hdbc;
            rc = SQLSetConnectAttr(hdbc, attrs[i], (SQLPOINTER)attrvals[i], 0);
        }
    }
    if (SQL_SUCCEEDED(rc)) {
        if (connect) {
            SQLCHAR out[1024];
            SQLSMALLINT outlen;

            err_step = "SQLDriverConnect";
            err_htype = SQL_HANDLE_DBC;
            err_h = hdbc;
            rc = SQLDriverConnect(hdbc, NULL, datasource,
                        (SQLSMALLINT)strlen((char *)datasource),
                        out, sizeof(out), &outlen, SQL_DRIVER_NOPROMPT);
        }
        else {
            err_step = "SQLConnect";
            err_htype = SQL_HANDLE_DBC;
            err_h = hdbc;
            rc = SQLConnect(hdbc, datasource,
                        (SQLSMALLINT)strlen((char *)datasource),
                        user, (SQLSMALLINT)strlen((char *)user),
                        password, (SQLSMALLINT)strlen((char *)password));
        }
    }
    if (SQL_SUCCEEDED(rc)) {
        handle = apr_pcalloc(pool, sizeof(apr_dbd_t));
        handle->dbname = apr_pstrdup(pool, (char *)datasource);
        handle->dbc = hdbc;
        handle->pool = pool;
        handle->defaultBufferSize = defaultBufferSize;
        CHECK_ERROR(handle, "SQLConnect", rc, SQL_HANDLE_DBC, handle->dbc);
        handle->default_transaction_mode = 0;
        handle->can_commit = APR_DBD_TRANSACTION_IGNORE_ERRORS;
        SQLGetInfo(hdbc, SQL_DEFAULT_TXN_ISOLATION,
                   &(handle->default_transaction_mode), sizeof(int), NULL);
        handle->transaction_mode = handle->default_transaction_mode;
        SQLGetInfo(hdbc, SQL_GETDATA_EXTENSIONS ,&(handle->dboptions),
                   sizeof(int), NULL);
        apr_pool_cleanup_register(pool, handle, odbc_close_cleanup, apr_pool_cleanup_null);
        return handle;
    }
    else {
        apr_dbd_t tmp_dbc;

        tmp_dbc.pool = pool;
        tmp_dbc.dbname = NULL;
        CHECK_ERROR(&tmp_dbc, err_step, rc, err_htype, err_h);
        if (error)
            *error = apr_pstrdup(pool, tmp_dbc.lastError);
        if (hdbc)
            SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
        return NULL;
    }
}

/** check_conn: check status of a database connection **/
static apr_status_t odbc_check_conn(apr_pool_t *pool, apr_dbd_t *handle)
{
    SQLUINTEGER isDead;
    SQLRETURN   rc;

    rc = SQLGetConnectAttr(handle->dbc, SQL_ATTR_CONNECTION_DEAD, &isDead,
                            sizeof(SQLUINTEGER), NULL);
    CHECK_ERROR(handle, "SQLGetConnectAttr (SQL_ATTR_CONNECTION_DEAD)", rc,
                SQL_HANDLE_DBC, handle->dbc);
    /* if driver cannot check connection, say so */
    if (rc != SQL_SUCCESS)
        return APR_ENOTIMPL;

    return (isDead == SQL_CD_FALSE) ? APR_SUCCESS : APR_EGENERAL;
}

/** set_dbname: select database name.  May be a no-op if not supported. **/
static int odbc_set_dbname(apr_pool_t*pool, apr_dbd_t *handle,
                           const char *name)
{
    if (apr_strnatcmp(name, handle->dbname)) {
        return APR_EGENERAL;        /* It's illegal to change dbname in ODBC */
    }
    CHECK_ERROR(handle, "set_dbname (no-op)", SQL_SUCCESS, SQL_HANDLE_DBC,
                handle->dbc);
    return APR_SUCCESS;             /* OK if it's the same name */
}

/** transaction: start a transaction.  May be a no-op. **/
static int odbc_start_transaction(apr_pool_t *pool, apr_dbd_t *handle,
                                  apr_dbd_transaction_t **trans)
{
    SQLRETURN rc = SQL_SUCCESS;

    if (handle->transaction_mode) {
        rc = SQLSetConnectAttr(handle->dbc, SQL_ATTR_TXN_ISOLATION,
                               (SQLPOINTER)handle->transaction_mode, 0);
        CHECK_ERROR(handle, "SQLSetConnectAttr (SQL_ATTR_TXN_ISOLATION)", rc,
                    SQL_HANDLE_DBC, handle->dbc);
    }
    if (SQL_SUCCEEDED(rc)) {
        /* turn off autocommit for transactions */
        rc = SQLSetConnectAttr(handle->dbc, SQL_ATTR_AUTOCOMMIT,
                               SQL_AUTOCOMMIT_OFF, 0);
        CHECK_ERROR(handle, "SQLSetConnectAttr (SQL_ATTR_AUTOCOMMIT)", rc,
                    SQL_HANDLE_DBC, handle->dbc);
    }
    if (SQL_SUCCEEDED(rc)) {
        *trans = apr_palloc(pool, sizeof(apr_dbd_transaction_t));
        (*trans)->dbc = handle->dbc;
        (*trans)->apr_dbd = handle;
    }
    handle->can_commit = APR_DBD_TRANSACTION_COMMIT;
    return APR_FROM_SQL_RESULT(rc);
}

/** end_transaction: end a transaction **/
static int odbc_end_transaction(apr_dbd_transaction_t *trans)
{
    SQLRETURN rc;
    int action = (trans->apr_dbd->can_commit != APR_DBD_TRANSACTION_ROLLBACK) 
        ? SQL_COMMIT : SQL_ROLLBACK;

    rc = SQLEndTran(SQL_HANDLE_DBC, trans->dbc, action);
    CHECK_ERROR(trans->apr_dbd, "SQLEndTran", rc, SQL_HANDLE_DBC, trans->dbc);
    if (SQL_SUCCEEDED(rc)) {
        rc = SQLSetConnectAttr(trans->dbc, SQL_ATTR_AUTOCOMMIT,
                               (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
        CHECK_ERROR(trans->apr_dbd, "SQLSetConnectAttr (SQL_ATTR_AUTOCOMMIT)",
                    rc, SQL_HANDLE_DBC, trans->dbc);
    }
    trans->apr_dbd->can_commit = APR_DBD_TRANSACTION_IGNORE_ERRORS;
    return APR_FROM_SQL_RESULT(rc);
}

/** query: execute an SQL statement which doesn't return a result set **/
static int odbc_query(apr_dbd_t *handle, int *nrows, const char *statement)
{
    SQLRETURN rc;
    SQLHANDLE hstmt = NULL;
    size_t len = strlen(statement);

    if (odbc_check_rollback(handle))
        return APR_EGENERAL;

    rc = SQLAllocHandle(SQL_HANDLE_STMT, handle->dbc, &hstmt);
    CHECK_ERROR(handle, "SQLAllocHandle (STMT)", rc, SQL_HANDLE_DBC,
                handle->dbc);
    if (!SQL_SUCCEEDED(rc))
        return APR_FROM_SQL_RESULT(rc);

    rc = SQLExecDirect(hstmt, (SQLCHAR *)statement, (SQLINTEGER)len);
    CHECK_ERROR(handle, "SQLExecDirect", rc, SQL_HANDLE_STMT, hstmt);

    if (SQL_SUCCEEDED(rc)) {
        SQLLEN rowcount;

        rc = SQLRowCount(hstmt, &rowcount);
        *nrows = (int)rowcount;
        CHECK_ERROR(handle, "SQLRowCount", rc, SQL_HANDLE_STMT, hstmt);
    }

    SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
    return APR_FROM_SQL_RESULT(rc);
}

/** select: execute an SQL statement which returns a result set **/
static int odbc_select(apr_pool_t *pool, apr_dbd_t *handle,
                       apr_dbd_results_t **res, const char *statement,
                       int random)
{
    SQLRETURN rc;
    SQLHANDLE hstmt;
    apr_dbd_prepared_t *stmt;
    size_t len = strlen(statement);

    if (odbc_check_rollback(handle))
        return APR_EGENERAL;

    rc = SQLAllocHandle(SQL_HANDLE_STMT, handle->dbc, &hstmt);
    CHECK_ERROR(handle, "SQLAllocHandle (STMT)", rc, SQL_HANDLE_DBC,
                handle->dbc);
    if (!SQL_SUCCEEDED(rc))
        return APR_FROM_SQL_RESULT(rc);
    /* Prepare an apr_dbd_prepared_t for pool cleanup, even though this
     * is not a prepared statement.  We want the same cleanup mechanism.
     */
    stmt = apr_pcalloc(pool, sizeof(apr_dbd_prepared_t));
    stmt->apr_dbd = handle;
    stmt->dbc = handle->dbc;
    stmt->stmt = hstmt;
    apr_pool_cleanup_register(pool, stmt, odbc_close_pstmt, apr_pool_cleanup_null);
    if (random) {
        rc = SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_SCROLLABLE,
                            (SQLPOINTER)SQL_SCROLLABLE, 0);
        CHECK_ERROR(handle, "SQLSetStmtAttr (SQL_ATTR_CURSOR_SCROLLABLE)", rc,
                    SQL_HANDLE_STMT, hstmt);
    }
    if (SQL_SUCCEEDED(rc)) {
        rc = SQLExecDirect(hstmt, (SQLCHAR *)statement, (SQLINTEGER)len);
        CHECK_ERROR(handle, "SQLExecDirect", rc, SQL_HANDLE_STMT, hstmt);
    }
    if (SQL_SUCCEEDED(rc)) {
        rc = odbc_create_results(handle, hstmt, pool, random, res);
        apr_pool_cleanup_register(pool, *res, 
                                  odbc_close_results, apr_pool_cleanup_null);
    }
    return APR_FROM_SQL_RESULT(rc);
}

/** num_cols: get the number of columns in a results set **/
static int odbc_num_cols(apr_dbd_results_t *res)
{
    return res->ncols;
}

/** num_tuples: get the number of rows in a results set **/
static int odbc_num_tuples(apr_dbd_results_t *res)
{
    SQLRETURN rc;
    SQLLEN nrows;

    rc = SQLRowCount(res->stmt, &nrows);
    CHECK_ERROR(res->apr_dbd, "SQLRowCount", rc, SQL_HANDLE_STMT, res->stmt);
    return SQL_SUCCEEDED(rc) ? (int)nrows : -1;
}

/** get_row: get a row from a result set **/
static int odbc_get_row(apr_pool_t *pool, apr_dbd_results_t *res,
                        apr_dbd_row_t **row, int rownum)
{
    SQLRETURN rc;
    char *fetchtype;
    int c;

    *row = apr_pcalloc(pool, sizeof(apr_dbd_row_t));
    (*row)->stmt = res->stmt;
    (*row)->dbc = res->dbc;
    (*row)->res = res;
    (*row)->pool = res->pool;

    /* mark all the columns as needing SQLGetData unless they are bound  */
    for (c = 0; c < res->ncols; c++) {
        if (res->colstate[c] != COL_BOUND) {
            res->colstate[c] = COL_AVAIL;
        }
        /* some drivers do not null-term zero-len CHAR data */
        if (res->colptrs[c])
            *(char *)res->colptrs[c] = 0; 
    }

    if (res->random && (rownum > 0)) {
        fetchtype = "SQLFetchScroll";
        rc = SQLFetchScroll(res->stmt, SQL_FETCH_ABSOLUTE, rownum);
    }
    else {
        fetchtype = "SQLFetch";
        rc = SQLFetch(res->stmt);
    }
    CHECK_ERROR(res->apr_dbd, fetchtype, rc, SQL_HANDLE_STMT, res->stmt);
    (*row)->stmt = res->stmt;
    if (!SQL_SUCCEEDED(rc) && !res->random) {
        /* early close on any error (usually SQL_NO_DATA) if fetching
         * sequentially to release resources ASAP
         */
        odbc_close_results(res);
        return -1;
    }
    return SQL_SUCCEEDED(rc) ? 0 : -1;
}

/** datum_get: get a binary entry from a row **/
static apr_status_t odbc_datum_get(const apr_dbd_row_t *row, int col,
                                   apr_dbd_type_e dbdtype, void *data)
{
    SQLSMALLINT sqltype;
    void *p;
    int len;

    if (col >= row->res->ncols)
        return APR_EGENERAL;

    if (dbdtype < 0 || dbdtype >= NUM_APR_DBD_TYPES) {
        data = NULL;            /* invalid type */
        return APR_EGENERAL;
    }

    len = sqlSizes[dbdtype];
    sqltype = sqlCtype[dbdtype];

    /* must not memcpy a brigade, sentinals are relative to orig loc */
    if (IS_LOB(sqltype)) 
        return odbc_create_bucket(row, col, sqltype, data);

    p = odbc_get(row, col, sqltype);
    if (p == (void *)-1)
        return APR_EGENERAL;

    if (p == NULL)
        return APR_ENOENT;          /* SQL NULL value */
    
    if (len < 0)
       *(char**)data = (char *)p;
    else
        memcpy(data, p, len);
    
    return APR_SUCCESS;

}

/** get_entry: get an entry from a row (string data) **/
static const char *odbc_get_entry(const apr_dbd_row_t *row, int col)
{
    void *p;

    if (col >= row->res->ncols)
        return NULL;

    p = odbc_get(row, col, SQL_C_CHAR);

    /* NULL or invalid (-1) */
    if (p == NULL || p == (void *)-1)
        return p;     
    else
        return apr_pstrdup(row->pool, p);   
}

/** error: get current error message (if any) **/
static const char *odbc_error(apr_dbd_t *handle, int errnum)
{   
    return (handle) ? handle->lastError : "[dbd_odbc]No error message available";
}

/** escape: escape a string so it is safe for use in query/select **/
static const char *odbc_escape(apr_pool_t *pool, const char *s,
                               apr_dbd_t *handle)
{   
    char *newstr, *src, *dst, *sq;
    int qcount;

    /* return the original if there are no single-quotes */
    if (!(sq = strchr(s, '\''))) 
        return (char *)s;
    /* count the single-quotes and allocate a new buffer */
    for (qcount = 1; (sq = strchr(sq + 1, '\'')); )
        qcount++;
    newstr = apr_palloc(pool, strlen(s) + qcount + 1);

    /* move chars, doubling all single-quotes */
    src = (char *)s;
    for (dst = newstr; *src; src++) {
        if ((*dst++ = *src) == '\'')  
            *dst++ = '\'';
    }
    *dst = 0;
    return newstr;
}

/** prepare: prepare a statement **/
static int odbc_prepare(apr_pool_t *pool, apr_dbd_t *handle,
                        const char *query, const char *label, int nargs,
                        int nvals, apr_dbd_type_e *types,
                        apr_dbd_prepared_t **statement)
{
    SQLRETURN rc;
    size_t len = strlen(query);

    if (odbc_check_rollback(handle))
        return APR_EGENERAL;

    *statement = apr_pcalloc(pool, sizeof(apr_dbd_prepared_t));
    (*statement)->dbc = handle->dbc;
    (*statement)->apr_dbd = handle;
    (*statement)->nargs = nargs;
    (*statement)->nvals = nvals;
    (*statement)->types =
        apr_pmemdup(pool, types, nargs * sizeof(apr_dbd_type_e));
    rc = SQLAllocHandle(SQL_HANDLE_STMT, handle->dbc, &((*statement)->stmt));
    apr_pool_cleanup_register(pool, *statement, 
                              odbc_close_pstmt, apr_pool_cleanup_null);
    CHECK_ERROR(handle, "SQLAllocHandle (STMT)", rc,
                SQL_HANDLE_DBC, handle->dbc);
    rc = SQLPrepare((*statement)->stmt, (SQLCHAR *)query, (SQLINTEGER)len);
    CHECK_ERROR(handle, "SQLPrepare", rc, SQL_HANDLE_STMT,
                (*statement)->stmt);
    return APR_FROM_SQL_RESULT(rc);
}

/** pquery: query using a prepared statement + args **/
static int odbc_pquery(apr_pool_t *pool, apr_dbd_t *handle, int *nrows,
                       apr_dbd_prepared_t *statement, const char **args)
{
    SQLRETURN rc = SQL_SUCCESS;
    int i, argp;

    if (odbc_check_rollback(handle))
        return APR_EGENERAL;

    for (i = argp = 0; i < statement->nargs && SQL_SUCCEEDED(rc); i++) {
        rc = odbc_bind_param(pool, statement, i + 1, statement->types[i],
                             &argp, (const void **)args, TEXTMODE);
    }
    if (SQL_SUCCEEDED(rc)) {
        rc = SQLExecute(statement->stmt);
        CHECK_ERROR(handle, "SQLExecute", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    if (SQL_SUCCEEDED(rc)) {
        SQLLEN rowcount;

        rc = SQLRowCount(statement->stmt, &rowcount);
        *nrows = (int)rowcount;
        CHECK_ERROR(handle, "SQLRowCount", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    return APR_FROM_SQL_RESULT(rc);
}

/** pvquery: query using a prepared statement + args **/
static int odbc_pvquery(apr_pool_t *pool, apr_dbd_t *handle, int *nrows,
                        apr_dbd_prepared_t *statement, va_list args)
{
    const char **values;
    int i;

    values = apr_palloc(pool, sizeof(*values) * statement->nvals);
    for (i = 0; i < statement->nvals; i++)
        values[i] = va_arg(args, const char *);
    return odbc_pquery(pool, handle, nrows, statement, values);
}

/** pselect: select using a prepared statement + args **/
static int odbc_pselect(apr_pool_t *pool, apr_dbd_t *handle,
                        apr_dbd_results_t **res, apr_dbd_prepared_t *statement,
                        int random, const char **args)
{
    SQLRETURN rc = SQL_SUCCESS;
    int i, argp;

    if (odbc_check_rollback(handle))
        return APR_EGENERAL;

    if (random) {
        rc = SQLSetStmtAttr(statement->stmt, SQL_ATTR_CURSOR_SCROLLABLE,
                            (SQLPOINTER)SQL_SCROLLABLE, 0);
        CHECK_ERROR(handle, "SQLSetStmtAttr (SQL_ATTR_CURSOR_SCROLLABLE)",
                    rc, SQL_HANDLE_STMT, statement->stmt);
    }
    if (SQL_SUCCEEDED(rc)) {
        for (i = argp = 0; i < statement->nargs && SQL_SUCCEEDED(rc); i++) {
            rc = odbc_bind_param(pool, statement, i + 1, statement->types[i],
                                 &argp, (const void **)args, TEXTMODE);
        }
    }
    if (SQL_SUCCEEDED(rc)) {
        rc = SQLExecute(statement->stmt);
        CHECK_ERROR(handle, "SQLExecute", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    if (SQL_SUCCEEDED(rc)) {
        rc = odbc_create_results(handle, statement->stmt, pool, random, res);
        apr_pool_cleanup_register(pool, *res,
                                  odbc_close_results, apr_pool_cleanup_null);
    }
    return APR_FROM_SQL_RESULT(rc);
}

/** pvselect: select using a prepared statement + args **/
static int odbc_pvselect(apr_pool_t *pool, apr_dbd_t *handle,
                         apr_dbd_results_t **res,
                         apr_dbd_prepared_t *statement, int random,
                         va_list args)
{
    const char **values;
    int i;

    values = apr_palloc(pool, sizeof(*values) * statement->nvals);
    for (i = 0; i < statement->nvals; i++)
        values[i] = va_arg(args, const char *);
    return odbc_pselect(pool, handle, res, statement, random, values);
}

/** get_name: get a column title from a result set **/
static const char *odbc_get_name(const apr_dbd_results_t *res, int col)
{
    SQLRETURN rc;
    char buffer[MAX_COLUMN_NAME];
    SQLSMALLINT colnamelength, coltype, coldecimal, colnullable;
    SQLULEN colsize;

    if (col >= res->ncols)
        return NULL;            /* bogus column number */
    if (res->colnames[col] != NULL)
        return res->colnames[col];      /* we already retrieved it */
    rc = SQLDescribeCol(res->stmt, col + 1,
                        (SQLCHAR *)buffer, sizeof(buffer), &colnamelength,
                        &coltype, &colsize, &coldecimal, &colnullable);
    CHECK_ERROR(res->apr_dbd, "SQLDescribeCol", rc,
                SQL_HANDLE_STMT, res->stmt);
    res->colnames[col] = apr_pstrdup(res->pool, buffer);
    return res->colnames[col];
}

/** transaction_mode_get: get the mode of transaction **/
static int odbc_transaction_mode_get(apr_dbd_transaction_t *trans)
{
    return (int)trans->apr_dbd->can_commit;
}

/** transaction_mode_set: set the mode of transaction **/
static int odbc_transaction_mode_set(apr_dbd_transaction_t *trans, int mode)
{
    int legal = (  APR_DBD_TRANSACTION_IGNORE_ERRORS
                 | APR_DBD_TRANSACTION_COMMIT
                 | APR_DBD_TRANSACTION_ROLLBACK);

    if ((mode & legal) != mode)
        return APR_EGENERAL;

    trans->apr_dbd->can_commit = mode;
    return APR_SUCCESS;
}

/** pbquery: query using a prepared statement + binary args **/
static int odbc_pbquery(apr_pool_t *pool, apr_dbd_t *handle, int *nrows,
                        apr_dbd_prepared_t *statement, const void **args)
{
    SQLRETURN rc = SQL_SUCCESS;
    int i, argp;

    if (odbc_check_rollback(handle))
        return APR_EGENERAL;

    for (i = argp = 0; i < statement->nargs && SQL_SUCCEEDED(rc); i++)
        rc = odbc_bind_param(pool, statement, i + 1, statement->types[i],
                             &argp, args, BINARYMODE);

    if (SQL_SUCCEEDED(rc)) {
        rc = SQLExecute(statement->stmt);
        CHECK_ERROR(handle, "SQLExecute", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    if (SQL_SUCCEEDED(rc)) {
        SQLLEN rowcount;

        rc = SQLRowCount(statement->stmt, &rowcount);
        *nrows = (int)rowcount;
        CHECK_ERROR(handle, "SQLRowCount", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    return APR_FROM_SQL_RESULT(rc);
}

/** pbselect: select using a prepared statement + binary args **/
static int odbc_pbselect(apr_pool_t *pool, apr_dbd_t *handle,
                         apr_dbd_results_t **res,
                         apr_dbd_prepared_t *statement,
                         int random, const void **args)
{
    SQLRETURN rc = SQL_SUCCESS;
    int i, argp;

    if (odbc_check_rollback(handle))
        return APR_EGENERAL;

    if (random) {
        rc = SQLSetStmtAttr(statement->stmt, SQL_ATTR_CURSOR_SCROLLABLE,
                            (SQLPOINTER)SQL_SCROLLABLE, 0);
        CHECK_ERROR(handle, "SQLSetStmtAttr (SQL_ATTR_CURSOR_SCROLLABLE)",
                    rc, SQL_HANDLE_STMT, statement->stmt);
    }
    if (SQL_SUCCEEDED(rc)) {
        for (i = argp = 0; i < statement->nargs && SQL_SUCCEEDED(rc); i++) {
            rc = odbc_bind_param(pool, statement, i + 1, statement->types[i],
                                 &argp, args, BINARYMODE);
        }
    }
    if (SQL_SUCCEEDED(rc)) {
        rc = SQLExecute(statement->stmt);
        CHECK_ERROR(handle, "SQLExecute", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    if (SQL_SUCCEEDED(rc)) {
        rc = odbc_create_results(handle, statement->stmt, pool, random, res);
        apr_pool_cleanup_register(pool, *res,
                                  odbc_close_results, apr_pool_cleanup_null);
    }

    return APR_FROM_SQL_RESULT(rc);
}

/** pvbquery: query using a prepared statement + binary args **/
static int odbc_pvbquery(apr_pool_t *pool, apr_dbd_t *handle, int *nrows,
                         apr_dbd_prepared_t *statement, va_list args)
{
    const char **values;
    int i;

    values = apr_palloc(pool, sizeof(*values) * statement->nvals);
    for (i = 0; i < statement->nvals; i++)
        values[i] = va_arg(args, const char *);
    return odbc_pbquery(pool, handle, nrows, statement, (const void **)values);
}

/** pvbselect: select using a prepared statement + binary args **/
static int odbc_pvbselect(apr_pool_t *pool, apr_dbd_t *handle,
                          apr_dbd_results_t **res,
                          apr_dbd_prepared_t *statement,
                          int random, va_list args)
{
    const char **values;
    int i;

    values = apr_palloc(pool, sizeof(*values) * statement->nvals);
    for (i = 0; i < statement->nvals; i++)
        values[i] = va_arg(args, const char *);
    return odbc_pbselect(pool, handle, res, statement, random, (const void **)values);
}

APU_MODULE_DECLARE_DATA const apr_dbd_driver_t ODBC_DRIVER_ENTRY = {
    ODBC_DRIVER_STRING,
    odbc_init,
    odbc_native_handle,
    odbc_open,
    odbc_check_conn,
    odbc_close,
    odbc_set_dbname,
    odbc_start_transaction,
    odbc_end_transaction,
    odbc_query,
    odbc_select,
    odbc_num_cols,
    odbc_num_tuples,
    odbc_get_row,
    odbc_get_entry,
    odbc_error,
    odbc_escape,
    odbc_prepare,
    odbc_pvquery,
    odbc_pvselect,
    odbc_pquery,
    odbc_pselect,
    odbc_get_name,
    odbc_transaction_mode_get,
    odbc_transaction_mode_set,
    "?",
    odbc_pvbquery,
    odbc_pvbselect,
    odbc_pbquery,
    odbc_pbselect,
    odbc_datum_get
};

#endif