/*
 * The contents of this file are subject to the AOLserver Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://aolserver.lcs.mit.edu/.
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 *
 * The Original Code is DB2 Driver for AOLserver 3.0 distributed by
 * Rob Mayoff <mayoff@dqd.com>.
 * 
 * The Initial Developer of the Original Code is Rob Mayoff
 * <mayoff@dqd.com>. Portions created by Rob Mayoff are
 * Copyright (C) 1999 Rob Mayoff.  All Rights Reserved.
 *
 * Alternatively, the contents of this file may be used under the terms
 * of the GNU General Public License (the "GPL"), in which case the
 * provisions of GPL are applicable instead of those above.  If you wish
 * to allow use of your version of this file only under the terms of the
 * GPL and not to allow others to use your version of this file under the
 * License, indicate your decision by deleting the provisions above and
 * replace them with the notice and other provisions required by the GPL.
 * If you do not delete the provisions above, a recipient may use your
 * version of this file under either the License or the GPL.
 */

static char     rcsid[] = "$Id: db2.c,v 1.7 1999/10/18 06:38:42 mayoff release-1_0_1 $";

int Ns_ModuleVersion = 1;
static char *DB2_driverName = "db2";

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <sqlcli1.h>
#include <assert.h>
#include "ns.h"

#define NullHandle 0
#define db2error(rc) ((rc) != SQL_SUCCESS && (rc) != SQL_SUCCESS_WITH_INFO)

#define checkEnvCode(rc, h) DB2_checkCode(handle, rc, SQL_HANDLE_ENV, h)
#define checkEnvCodeNoHandle(rc, h) DB2_checkCode(NULL, rc, SQL_HANDLE_ENV, h)
#define checkConnCode(rc, h) DB2_checkCode(handle, rc, SQL_HANDLE_DBC, h)
#define checkStmtCode(rc, h) DB2_checkCode(handle, rc, SQL_HANDLE_STMT, h)

static SQLHANDLE DB2_environment;
static Tcl_HashTable DB2_constants;

/*
 * The following should probably be configurable on a per-pool
 * basis...
 */
static const SQLINTEGER DB2_maxDataSize = 10 * 1024 * 1024;
static const size_t DB2_chunkSize = 64 * 1024;

extern int Ns_DbDriverInit(char *driver, char *configPath);

static int DB2_getConstant(char *name, int defaultValue);
static int DB2_checkCode(Ns_DbHandle *nsDbHandle, int rc,
    SQLSMALLINT handleType, SQLHANDLE handle);
static void DB2_shutdown(char *driver);
static int DB2_tclCommand(ClientData data, Tcl_Interp *interp,
    int argc, char **argv);
static int DB2_interpInit(Tcl_Interp *interp, void *data);
static int DB2_serverInit(char *server, char *module, char *driver);
static char *DB2_name(Ns_DbHandle *handle);
static char *DB2_dbType(Ns_DbHandle *handle);
static int DB2_openDb(Ns_DbHandle *handle);
static int DB2_closeDb(Ns_DbHandle *handle);
static int DB2_exec(Ns_DbHandle *handle, char *sql);
static int DB2_flush(Ns_DbHandle *handle);
static int DB2_cancel(Ns_DbHandle *handle);
static int DB2_reset(Ns_DbHandle *handle);
static Ns_Set *DB2_bindRow(Ns_DbHandle *handle);
static int DB2_getRow(Ns_DbHandle *handle, Ns_Set *row);
static int DB2_extendedExec(Tcl_Interp *interp, Ns_DbHandle *handle,
    char *sql, char *inputlist);
static int DB2_isFileName(char *string);
static int DB2_extendedGetRow(Tcl_Interp *interp, Ns_DbHandle *handle,
    Ns_Set *row, char *outputList);
static int DB2_bindFileColumns(Ns_DbHandle *handle, int argc, char **argv,
    SQLUINTEGER *fileOptionsPtr, SQLINTEGER *indicatorPtr);
static int DB2_sendColumn(Tcl_Interp *interp, Ns_DbHandle *handle,
    int column);
static void DB2_setException(Ns_DbHandle *handle, char *message);

static Ns_DbProc DB2Procs[] = {
    { DbFn_BindRow,     DB2_bindRow    },
    { DbFn_Cancel,      DB2_cancel     },
    { DbFn_CloseDb,     DB2_closeDb    },
    { DbFn_DbType,      DB2_dbType     },
    { DbFn_Exec,        DB2_exec       },
    { DbFn_Flush,       DB2_flush      },
    { DbFn_GetRow,      DB2_getRow     },
    { DbFn_Name,        DB2_name       },
    { DbFn_OpenDb,      DB2_openDb     },
    { DbFn_ResetHandle, DB2_reset      },
    { DbFn_ServerInit,  DB2_serverInit },
    /*
     * Note: DbFn_Select and DbFn_DML are not needed, because
     * Ns_DbSelect and Ns_DbDML prefer to use DbFn_Exec anyway.
     */
    { 0,                NULL           },
};

/*- API Functions ----------------------------------------------------*/

int
Ns_DbDriverInit(char *driver, char *configPath)
{
    SQLRETURN rc;

    Ns_Log(Notice, "%s: initializing with config path \"%s\"",
	driver, configPath);

    Tcl_InitHashTable(&DB2_constants, TCL_STRING_KEYS);

#define CONSTANT(constant) \
    do { \
	int isNew;\
	int *valuePtr = (int *)malloc(sizeof(int)); \
	*valuePtr = SQL_##constant; \
	Tcl_SetHashValue( \
	    Tcl_CreateHashEntry(&DB2_constants, #constant, &isNew), \
	    (ClientData)valuePtr); \
    } while (0)

    CONSTANT(TXN_READ_UNCOMMITTED);
    CONSTANT(TXN_READ_COMMITTED);
    CONSTANT(TXN_REPEATABLE_READ);
    CONSTANT(TXN_SERIALIZABLE);

    CONSTANT(BIGINT);
    CONSTANT(BINARY);
    CONSTANT(BLOB);
    CONSTANT(CHAR);
    CONSTANT(CLOB);
    CONSTANT(DATE);
    CONSTANT(DBCLOB);
    CONSTANT(DECIMAL);
    CONSTANT(DOUBLE);
    CONSTANT(FLOAT);
    CONSTANT(GRAPHIC);
    CONSTANT(INTEGER);
    CONSTANT(LONGVARBINARY);
    CONSTANT(LONGVARCHAR);
    CONSTANT(LONGVARGRAPHIC);
    CONSTANT(NUMERIC);
    CONSTANT(REAL);
    CONSTANT(SMALLINT);
    CONSTANT(TIME);
    CONSTANT(TIMESTAMP);
    CONSTANT(VARBINARY);
    CONSTANT(VARCHAR);
    CONSTANT(VARGRAPHIC);

#undef CONSTANT

    rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &DB2_environment);
    if (checkEnvCodeNoHandle(rc, DB2_environment) != NS_OK) {

 error:
	if (DB2_environment != NullHandle)
	    SQLFreeHandle(SQL_HANDLE_ENV, DB2_environment);
	return NS_ERROR;

    }

    rc = SQLSetEnvAttr(DB2_environment, SQL_ATTR_ODBC_VERSION,
	(SQLPOINTER)SQL_OV_ODBC3, 0);
    if (checkEnvCodeNoHandle(rc, DB2_environment) != NS_OK) goto error;

    rc = SQLSetEnvAttr(DB2_environment, SQL_ATTR_PROCESSCTL,
	(SQLPOINTER)SQL_PROCESSCTL_NOFORK, 0);
    if (checkEnvCodeNoHandle(rc, DB2_environment) != NS_OK) goto error;

    if (Ns_DbRegisterDriver(driver, DB2Procs) != NS_OK) {
	Ns_Log(Error, "Ns_DbRegisterDriver: error registering "
	    "db2 driver as %s", driver);
	return NS_ERROR;
    }

    Ns_RegisterShutdown((Ns_Callback *)DB2_shutdown, driver);

    return NS_OK;
}

/*- Callbacks --------------------------------------------------------*/

static void
DB2_shutdown(char *driver)
{
    Ns_Log(Notice, "%s: shutting down", driver);
    /*SQLFreeHandle(SQL_HANDLE_ENV, DB2_environment);*/
}

static int
DB2_interpInit(Tcl_Interp *interp, void *data)
{
    Tcl_CreateCommand(interp, "dqd_db2", DB2_tclCommand, NULL, NULL);
    return NS_OK;
}

/*- Database Driver callbacks ----------------------------------------*/

static int
DB2_serverInit(char *server, char *module, char *driver)
{
    return Ns_TclInitInterps(server, DB2_interpInit, NULL);
}

static char *
DB2_name(Ns_DbHandle *handle)
{
    return DB2_driverName;
}

static char *
DB2_dbType(Ns_DbHandle *handle)
{
    return DB2_driverName;
}

static int
DB2_openDb(Ns_DbHandle *handle)
{
    SQLRETURN rc;
    SQLHANDLE connection = NullHandle;
    char *configPath;

    Ns_Log(Notice, "%s: connecting to datasource \"%s\" "
	"with user \"%s\" and password \"%s\"", handle->driver,
	handle->datasource ? handle->datasource : "(null)",
	handle->user ? handle->user : "(null)",
	handle->password ? handle->password : "(null)");

    if (handle->datasource == NULL || handle->datasource[0] == '\000') {
	Ns_Log(Error, "%s: datasource is empty!",
	    handle->driver);
	return NS_ERROR;
    }

    configPath = Ns_ConfigGetPath(NULL, NULL, "db", "pool",
	handle->poolname, NULL);
    assert(configPath != NULL);

    rc = SQLAllocHandle(SQL_HANDLE_DBC, DB2_environment, &connection);
    if (checkEnvCode(rc, DB2_environment) != NS_OK) {

 error:
	if (connection != NullHandle)
	    SQLFreeHandle(SQL_HANDLE_DBC, connection);
	return NS_ERROR;

    }

    {
	int autoCommit = NS_FALSE;
	Ns_ConfigGetBool(configPath, "autoCommit", &autoCommit);
	rc = SQLSetConnectAttr(connection, SQL_ATTR_AUTOCOMMIT,
	    (SQLPOINTER)(autoCommit ? SQL_AUTOCOMMIT_ON
		: SQL_AUTOCOMMIT_OFF), 0);
	if (checkConnCode(rc, connection) != NS_OK) goto error;
    }

    rc = SQLConnect(connection,
	(SQLCHAR *)handle->datasource, SQL_NTS,
	(SQLCHAR *)handle->user,
	(handle->user == NULL) ? 0 : SQL_NTS,
	(SQLCHAR *)handle->password,
	(handle->password == NULL) ? 0 : SQL_NTS);
    if (checkConnCode(rc, connection) != NS_OK) goto error;

    rc = SQLSetConnectAttr(connection, SQL_ATTR_TXN_ISOLATION,
	(SQLPOINTER)DB2_getConstant(
	    Ns_ConfigGetValue(configPath, "txnIsolation"),
	    SQL_TXN_SERIALIZABLE),
	0);
    if (checkConnCode(rc, connection) != NS_OK) goto error;

    handle->connection = (void *)connection;
    handle->connected = NS_TRUE;

    Ns_Log(Notice, "%s: got connection %d",
	handle->driver, connection);

    return NS_OK;
}

static int
DB2_closeDb(Ns_DbHandle *handle)
{
    SQLRETURN rc;
    SQLHANDLE connection;
    int errors = 0;

    connection = (SQLHANDLE)handle->connection;

    Ns_Log(Notice, "%s: closing connection %d", handle->driver, connection);

    rc = SQLDisconnect(connection);
    if (checkConnCode(rc, connection) != NS_OK) errors++;

    rc = SQLFreeHandle(SQL_HANDLE_DBC, connection);
    if (checkEnvCode(rc, DB2_environment) != NS_OK) errors++;

    handle->connection = NULL;
    handle->connected = NS_FALSE;

    return errors ? NS_ERROR : NS_OK;
}

static int
DB2_exec(Ns_DbHandle *handle, char *sql)
{
    return DB2_extendedExec(NULL, handle, sql, NULL);
}

static int
DB2_flush(Ns_DbHandle *handle)
{
    SQLHANDLE statement = (SQLHANDLE)handle->statement;
    SQLRETURN rc;

    Ns_Log(Debug, "%s: flush", handle->driver);

    if (statement == NullHandle) {
	Ns_Log(Debug, "%s: asked to flush with no statement", handle->driver);
	Ns_DbSetException(handle, "", "");
    }

    else {
	rc = SQLFreeHandle(SQL_HANDLE_STMT, statement);
	if (checkStmtCode(rc, statement) != NS_OK)
	    return NS_ERROR;

	handle->statement = NULL;
	handle->fetchingRows = NS_FALSE;
    }

    return NS_OK;
}

static int
DB2_cancel(Ns_DbHandle *handle)
{
    SQLHANDLE statement = (SQLHANDLE)handle->statement;
    SQLRETURN rc;
    int status = NS_OK;

    Ns_Log(Debug, "%s: cancel", handle->driver);

    if (statement == NullHandle) {
	Ns_Log(Debug, "%s: asked to cancel with no statement",
	    handle->driver);
	Ns_DbSetException(handle, "", "");
    }

    else {
	rc = SQLCancel(statement);
	if (checkStmtCode(rc, statement) != NS_OK) status = NS_ERROR;
	rc = SQLFreeHandle(SQL_HANDLE_STMT, statement);
	if (checkStmtCode(rc, statement) != NS_OK) status = NS_ERROR;

	handle->statement = NULL;
	handle->fetchingRows = 0;
    }

    return status;
}

static int
DB2_reset(Ns_DbHandle *handle)
{
    SQLHANDLE connection = (SQLHANDLE)handle->connection;
    SQLRETURN rc;

    Ns_Log(Debug, "%s: reset", handle->driver);

    if (DB2_cancel(handle) != NS_OK)
	return NS_ERROR;

    rc = SQLEndTran(SQL_HANDLE_DBC, connection, SQL_ROLLBACK);
    if (checkConnCode(rc, connection) != NS_OK)
	return NS_ERROR;

    return NS_OK;
}

static Ns_Set *
DB2_bindRow(Ns_DbHandle *handle)
{
    SQLHANDLE statement = (SQLHANDLE)handle->statement;
    SQLRETURN rc;
    SQLSMALLINT columnsCount, i;
    Ns_Set *row = handle->row;

    if (!handle->fetchingRows) {
	Ns_Log(Error, "%s: bindRow called with no SELECT statement",
	    handle->driver);
	DB2_setException(handle, "bindRow called with no SELECT statement");
	return NULL;
    }

    rc = SQLNumResultCols(statement, &columnsCount);
    if (checkStmtCode(rc, statement) != NS_OK) {
 error:
	SQLFreeHandle(SQL_HANDLE_STMT, statement);
	handle->statement = NULL;
	handle->fetchingRows = 0;
	return NULL;
    }

    for (i = 1; i <= columnsCount; i++) {
	SQLCHAR columnName[100];
	SQLSMALLINT columnNameLength;
	SQLCHAR *p;

	rc = SQLColAttribute(statement, i, SQL_DESC_LABEL,
	    (SQLPOINTER)columnName, sizeof columnName,
	    &columnNameLength, NULL);
	if (checkStmtCode(rc, statement) != NS_OK) goto error;
	for (p = columnName; *p; p++) *p = tolower(*p);
	Ns_SetPut(row, columnName, NULL);
    }

    return row;
}

static int
DB2_getRow(Ns_DbHandle *handle, Ns_Set *row)
{
    return DB2_extendedGetRow(NULL, handle, row, NULL);
}

/*- DB2 Driver Tcl commands ------------------------------------------*/

static int
DB2_tclCommand(ClientData data, Tcl_Interp *interp, int argc, char **argv)
{
    Ns_DbHandle *handle;
    int rc;

    if (argc < 2) {
	Tcl_AppendResult(interp, argv[0], ": missing subcommand", NULL);
	return TCL_ERROR;
    }

    if (argc < 3) {
	Tcl_AppendResult(interp, argv[0], ": missing database handle", NULL);
	return TCL_ERROR;
    }

    if (Ns_TclDbGetHandle(interp, argv[2], &handle) != TCL_OK) {
	return TCL_ERROR;
    }

    if (Ns_DbDriverName(handle) != DB2_driverName) {
	Tcl_AppendResult(interp, "handle ", argv[1],
	    " is not a db2 handle", NULL);
	return TCL_ERROR;
    }

    if (STREQ(argv[1], "exec")) {
	if (argc != 5) {
	    Tcl_AppendResult(interp, "usage: ", argv[0], " ", argv[1],
		" <dbhandle> <sql-statement> <input-list>", NULL);
	    return TCL_ERROR;
	}

	rc = DB2_extendedExec(interp, handle, argv[3], argv[4]);
	switch (rc) {
	    case NS_OK:
		Tcl_SetResult(interp, NULL, TCL_STATIC);
		return TCL_OK;
	    case NS_ROWS:
		Tcl_SetResult(interp, "NS_ROWS", TCL_STATIC);
		return TCL_OK;
	    case NS_DML:
		Tcl_SetResult(interp, "NS_DML", TCL_STATIC);
		return TCL_OK;
	    default:
		return TCL_ERROR;
	}
    }

    else if (STREQ(argv[1], "getrow")) {
	if (argc != 4) {
	    Tcl_AppendResult(interp, "usage: ", argv[0], " ", argv[1],
		" <dbhandle> <output-list>", NULL);
	    return TCL_ERROR;
	}

	rc = DB2_extendedGetRow(interp, handle, NULL, argv[3]);
	switch (rc) {
	    case NS_OK:
		Tcl_SetResult(interp, "1", TCL_STATIC);
		return TCL_OK;
	    case NS_END_DATA:
		Tcl_SetResult(interp, "0", TCL_STATIC);
		return TCL_OK;
	    default:
		return TCL_ERROR;
	}
    }

    else if (STREQ(argv[1], "sendcolumn")) {
	int column;

	if (argc != 4) {
	    Tcl_AppendResult(interp, "usage: ", argv[0], " ", argv[1],
		" <dbhandle> <column-number>", NULL);
	    return TCL_ERROR;
	}

	if (Tcl_GetInt(interp, argv[3], &column) != TCL_OK)
	    return TCL_ERROR;

	rc = DB2_sendColumn(interp, handle, column);
	switch (rc) {
	    case NS_OK:
		Tcl_SetResult(interp, "1", TCL_STATIC);
		return TCL_OK;
	    case NS_END_DATA:
		Tcl_SetResult(interp, "0", TCL_STATIC);
		return TCL_OK;
	    default:
		return TCL_ERROR;
	}
    }

    Tcl_AppendResult(interp, argv[0], ": invalid subcommand ", argv[1], NULL);
    return TCL_ERROR;
}

/*- Internal functions -----------------------------------------------*/


static void
DB2_setException(Ns_DbHandle *handle, char *message)
{
    Ns_DbSetException(handle, "NSDB2", message);
}

static int
DB2_getConstant(char *name, int defaultValue)
{
    /* Assumption: no constants have names longer than this... */
    char upperCaseName[200];
    int i;
    Tcl_HashEntry *entry;

    if (name == NULL || *name == '\000')
	return defaultValue;

    for (i = 0; i < sizeof upperCaseName - 1 && name[i]; i++)
	upperCaseName[i] = toupper(name[i]);
    upperCaseName[i] = 0;

    entry = Tcl_FindHashEntry(&DB2_constants, upperCaseName);
    if (entry == NULL) {
	Ns_Log(Error, "db2: could not find value for \"%s\"", name);
	return defaultValue;
    }

    return *(int *)Tcl_GetHashValue(entry);
}

static int
DB2_checkCode(Ns_DbHandle *nsDbHandle, int rc,
    SQLSMALLINT handleType, SQLHANDLE handle)
{
    int status;
    char codeString[100];
    SQLCHAR sqlState[6];
    SQLINTEGER nativeError = 0;
    char message[500];

    if (rc == SQL_SUCCESS) {
	if (nsDbHandle != NULL)
	    Ns_DbSetException(nsDbHandle, "", "");
	return NS_OK;
    }

    switch (rc) {

#define CODECASE(code, s) \
	case SQL_##code: \
	    strcpy(codeString, "SQL_" #code); \
	    status = s; \
	    break

	CODECASE(SUCCESS_WITH_INFO, NS_OK);
	CODECASE(NO_DATA_FOUND,     NS_OK);
	CODECASE(STILL_EXECUTING,   NS_ERROR);
	CODECASE(NEED_DATA,         NS_ERROR);
	CODECASE(ERROR,             NS_ERROR);
	CODECASE(INVALID_HANDLE,    NS_ERROR);

#undef CODECASE

	default:
	    sprintf(codeString, "Unknown SQL return code %d", rc);
	    status = NS_ERROR;
	    break;
    }

    if (handle == NullHandle) {
	sqlState[0] = '\000';
	nativeError = 0;
	strcpy(message, "(no database error message available)");
    }

    else {
	SQLGetDiagRec(handleType, handle, 1, sqlState, &nativeError,
	    message, sizeof message, NULL);

	if (nsDbHandle != NULL)
	    Ns_DbSetException(nsDbHandle, sqlState, message);
    }

    Ns_Log(status == NS_OK ? Notice : Error,
	"DB2 return code %s; sqlState = [%s]; "
	"nativeError = %d; DB2 error message = \"%s\"",
	codeString, sqlState, nativeError, message);

    return status;
}

static int
DB2_isFileName(char *string)
{
    return *string == '.' || *string == '/';
}

static int
DB2_extendedExec(Tcl_Interp *interp, Ns_DbHandle *handle,
    char *sql, char *inputList)
{
    SQLHANDLE connection = (SQLHANDLE)handle->connection;
    SQLHANDLE statement = NullHandle;
    SQLRETURN rc;
    SQLSMALLINT columnsCount = 0;
    int argc = 0;
    char **argv = NULL;
    int i;
    SQLUINTEGER fileOptions = SQL_FILE_READ;
    SQLINTEGER indicator = 0;

    rc = SQLAllocHandle(SQL_HANDLE_STMT, connection, &statement);
    if (checkConnCode(rc, connection) != NS_OK) {

 error:
	if (interp != NULL) {
	    Tcl_AppendResult(interp, handle->driver,
		": database error: SQLSTATE=",
		handle->cExceptionCode, "; message=\"",
		Ns_DStringValue(&handle->dsExceptionMsg), "\"",
		NULL);
	}
	if (argv != NULL)
	    Tcl_Free((char *)argv);
	if (statement != NullHandle)
	    SQLFreeHandle(SQL_HANDLE_STMT, statement);
	return NS_ERROR;

    }

    if (inputList != NULL) 
    {
	if (interp == NULL) {
	    DB2_setException(handle,
		"exec called with input list but no Tcl interpreter");
	    goto error;
	}

	if (Tcl_SplitList(interp, inputList, &argc, &argv) != TCL_OK)
	    goto error;

	if (argc & 1) {
	    DB2_setException(handle,
		"exec input list contains odd number of elements");
	    goto error;
	}

	for (i = 0; i < argc; i++) {
	    SQLSMALLINT sqlType;

	    sqlType = DB2_getConstant(argv[i], SQL_VARCHAR);

	    i++;

	    if (DB2_isFileName(argv[i])) {
		if (sqlType != SQL_BLOB && sqlType != SQL_CLOB
		    && sqlType != SQL_DBCLOB)
		{
		    DB2_setException(handle,
			"filename passed as input for non-LOB parameter");
		    goto error;
		}

		rc = SQLBindFileToParam(statement, 1 + i/2, sqlType,
		    argv[i], NULL, &fileOptions, strlen(argv[i]) + 1,
		    &indicator);
	    }

	    else {
		SQLINTEGER indicator = SQL_NTS;
		char *value = Tcl_GetVar(interp, argv[i],
		    TCL_LEAVE_ERR_MSG);
		if (value == NULL) goto error;

		rc = SQLBindParameter(statement, 1 + i/2,
		    SQL_PARAM_INPUT, SQL_C_CHAR, sqlType, 0,
		    0, (SQLPOINTER)value, 0, &indicator);
	    }

	    if (checkStmtCode(rc, statement) != NS_OK) {
		goto error;

	    }
	}
    }

    rc = SQLExecDirect(statement, sql, SQL_NTS);
    if (checkStmtCode(rc, statement) != NS_OK) goto error;

    if (argv != NULL) {
	Tcl_Free((char *)argv);
	argv = NULL;
    }

    rc = SQLNumResultCols(statement, &columnsCount);
    if (checkStmtCode(rc, statement) != NS_OK) goto error;
    if (columnsCount > 0) {
	handle->statement = (void *)statement;
	handle->fetchingRows = 1;
	return NS_ROWS;
    }

    else {
	SQLFreeHandle(SQL_HANDLE_STMT, statement);
	return NS_DML;
    }
}

static int
DB2_bindFileColumns(Ns_DbHandle *handle, int argc, char **argv,
    SQLUINTEGER *fileOptionsPtr, SQLINTEGER *indicatorPtr)
{
    SQLHANDLE statement = (SQLHANDLE)handle->statement;
    SQLRETURN rc;
    int i;

    for (i = 0; i < argc; i++) {
	if (!DB2_isFileName(argv[i])) continue;

	rc = SQLBindFileToCol(statement, i + 1, argv[i], NULL,
	    fileOptionsPtr, 0, NULL, indicatorPtr);
	if (checkStmtCode(rc, statement) != NS_OK)
	    return NS_ERROR;
    }

    return NS_OK;
}

static int
DB2_getColumn(Tcl_Interp *interp, Ns_DbHandle *handle, int column,
    Ns_Set *row, char *outputName)
{
    SQLHANDLE statement = (SQLHANDLE)handle->statement;
    SQLRETURN rc;
    SQLINTEGER indicator;
    char buffer[8192];
    char *value = buffer;
    int status = NS_OK;

    rc = SQLGetData(statement, column + 1, SQL_C_CHAR, 
	(SQLPOINTER)buffer, sizeof buffer, &indicator);

    if (rc == SQL_SUCCESS_WITH_INFO && indicator >= sizeof buffer) {
	/* buffer too small */
	int size = indicator;
	if (size > DB2_maxDataSize) {
	    size = DB2_maxDataSize;
	    Ns_Log(Warning, "%s: data too big (%d bytes); "
		"truncating to %d bytes\n", indicator, size);
	}
	value = Ns_Malloc(size + 1);

	/*
	 * I'm letting memcpy copy the trailing NUL because sizeof
	 * buffer is a power of 2 and maybe memcpy is faster in
	 * that case.
	 */
	memcpy(value, buffer, sizeof buffer);

	rc = SQLGetData(statement, column + 1, SQL_C_CHAR,
	    (SQLPOINTER)(value + sizeof buffer - 1),
	    size - (sizeof buffer - 1), &indicator);

	if (checkStmtCode(rc, statement) != NS_OK) {
	    status = NS_ERROR;
	    goto done;
	}
    }

    else if (checkStmtCode(rc, statement) != NS_OK)
	goto done;

    else if (indicator == SQL_NULL_DATA) {
	*value = '\000';
    }

    if (outputName != NULL) {
	if (Tcl_SetVar(interp, outputName, value, TCL_LEAVE_ERR_MSG) == NULL)
	    status = NS_ERROR;
    }

    else Ns_SetPutValue(row, column, buffer);

 done:
    if (value != buffer)
	Ns_Free(value);
    return status;
}

static int
DB2_extendedGetRow(Tcl_Interp *interp, Ns_DbHandle *handle,
    Ns_Set *row, char *outputList)
{
    SQLHANDLE statement = (SQLHANDLE)handle->statement;
    SQLRETURN rc;
    SQLSMALLINT i, max;
    int argc = 0;
    char **argv = NULL;
    SQLUINTEGER fileOptions = SQL_FILE_OVERWRITE;
    SQLINTEGER indicator;


    if (!handle->fetchingRows) {
	DB2_setException(handle, "getRow called with no SELECT statement");
	Ns_Log(Error, "%s: %s", handle->driver,
	    Ns_DStringValue(&handle->dsExceptionMsg));
	return NS_ERROR;
    }

    if (outputList != NULL) {

	if (interp == NULL) {

	    DB2_setException(handle,
		"getrow called with output list but no Tcl interpreter");

 error:
	    if (interp != NULL) {
		Tcl_AppendResult(interp, handle->driver,
		    ": database error: SQLSTATE=",
		    handle->cExceptionCode, "; message=\"",
		    Ns_DStringValue(&handle->dsExceptionMsg), "\"",
		    NULL);
	    }
	    if (argv != NULL) Tcl_Free((char *)argv);
	    SQLFreeHandle(SQL_HANDLE_STMT, statement);
	    handle->statement = NULL;
	    handle->fetchingRows = 0;
	    return NS_ERROR;

	}

	if (Tcl_SplitList(interp, outputList, &argc, &argv) != TCL_OK)
	    goto error;

	if (DB2_bindFileColumns(handle, argc, argv,
		&fileOptions, &indicator) != NS_OK)
	    goto error;
    }

    rc = SQLFetch(statement);

    if (rc == SQL_NO_DATA_FOUND) {
	handle->statement = NULL;
	handle->fetchingRows = 0;
	rc = SQLFreeHandle(SQL_HANDLE_STMT, statement);
	if (checkStmtCode(rc, statement) != NS_OK) return NS_ERROR;
	return NS_END_DATA;
    }

    if (checkStmtCode(rc, statement) != NS_OK)
	goto error;

    max = (argv != NULL) ? argc : Ns_SetSize(row);

    for (i = 0; i < max; i++) {
	if (argv == NULL)
	    rc = DB2_getColumn(interp, handle, i, row, NULL);

	else if (!DB2_isFileName(argv[i]))
	    rc = DB2_getColumn(interp, handle, i, NULL, argv[i]);

	else continue;

	if (rc != NS_OK)
	    goto error;
    }

    rc = SQLFreeStmt(statement, SQL_UNBIND);
    if (checkStmtCode(rc, statement) != NS_OK)
	goto error;

    if (argv != NULL)
	Tcl_Free((char *)argv);

    return NS_OK;
}

static int
DB2_sendColumn(Tcl_Interp *interp, Ns_DbHandle *handle, int column)
{
    SQLHANDLE statement = (SQLHANDLE)handle->statement;
    SQLRETURN rc;
    SQLINTEGER indicator;
    char *buffer = NULL;
    Ns_Conn *conn;

    if (!handle->fetchingRows) {
	DB2_setException(handle,
	    "sendColumn called without a SELECT statement");
	Ns_Log(Error, "%s: %s", handle->driver,
	    Ns_DStringValue(&handle->dsExceptionMsg));
    }

    rc = SQLFetch(statement);

    if (rc == SQL_NO_DATA_FOUND) {
	handle->statement = NULL;
	handle->fetchingRows = 0;
	rc = SQLFreeHandle(SQL_HANDLE_STMT, statement);
	if (checkStmtCode(rc, statement) != NS_OK) return NS_ERROR;
	return NS_END_DATA;
    }

    if (checkStmtCode(rc, statement) != NS_OK) {

 error:
	if (interp != NULL) {
	    Tcl_AppendResult(interp, handle->driver,
		": database error: SQLSTATE=",
		handle->cExceptionCode, "; message=\"",
		Ns_DStringValue(&handle->dsExceptionMsg), "\"",
		NULL);
	}
	SQLFreeHandle(SQL_HANDLE_STMT, statement);
	handle->statement = NULL;
	handle->fetchingRows = 0;
	if (buffer != NULL) Ns_Free(buffer);
	return NS_ERROR;

    }

    buffer = Ns_Malloc(DB2_chunkSize);
    conn = Ns_TclGetConn(interp);

    do {
	rc = SQLGetData(statement, column + 1, SQL_C_BINARY,
	    (SQLPOINTER)buffer, DB2_chunkSize, &indicator);


	if (rc == SQL_SUCCESS_WITH_INFO && indicator >= DB2_chunkSize)
	    rc = SQL_SUCCESS;

	if (checkStmtCode(rc, statement) != NS_OK)
	    goto error;

	if (indicator == SQL_NULL_DATA)
	    break;

	if (Ns_WriteConn(conn, buffer, indicator >= DB2_chunkSize
		? DB2_chunkSize - 1: indicator) != NS_OK)
	    goto error;

    } while (indicator >= DB2_chunkSize);

    Ns_Free(buffer);
    return NS_OK;
}

