/*
 * 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.com/.
 *
 * 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 AOLserver Code and related documentation
 * distributed by AOL.
 * 
 * The Initial Developer of the Original Code is America Online,
 * Inc. Portions created by AOL are Copyright (C) 1999 America Online,
 * Inc. 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.
 */

/* 
 * dbtcl.c --
 *
 *	Tcl database access routines.
 */

static const char *RCSID = "@(#) $Header: /cvsroot/aolserver/aolserver/nsdb/dbtcl.c,v 1.6 2005/03/28 00:13:54 jgdavidson Exp $, compiled: " __DATE__ " " __TIME__;

#include "db.h"

/*
 * The following structure maintains per-interp data.
 */

typedef struct InterpData {
    Tcl_Interp *interp;
    char *server;
    int   cleanup;
    Tcl_HashTable dbs;
} InterpData;

/*
 * Local functions defined in this file
 */

static void EnterRow(Tcl_Interp *interp, Ns_Set *row, int flags,
		     int *statusPtr);
static void EnterHandle(InterpData *idataPtr, Tcl_Interp *interp,
			  Ns_DbHandle *handle);
static int GetHandle(InterpData *idataPtr, char *id,
		     Ns_DbHandle **handle, int clear, Tcl_HashEntry **hPtrPtr);
static int GetHandleObj(InterpData *idataPtr, Tcl_Obj *obj,
		     Ns_DbHandle **handle, int clear, Tcl_HashEntry **hPtrPtr);
static Tcl_InterpDeleteProc FreeData;
static Ns_TclDeferProc ReleaseDbs;
static Tcl_ObjCmdProc DbObjCmd;
static Tcl_CmdProc QuoteListToListCmd, GetCsvCmd, DbErrorCodeCmd,
	DbErrorMsgCmd, GetCsvCmd, DbConfigPathCmd, PoolDescriptionCmd;
static char *datakey = "nsdb:data";


/*
 *----------------------------------------------------------------------
 * Ns_TclDbGetHandle --
 *
 *      Get database handle from its handle id.
 *
 * Results:
 *      See GetHandle().
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

int
Ns_TclDbGetHandle(Tcl_Interp *interp, char *id, Ns_DbHandle **handle)
{
    InterpData *idataPtr;

    idataPtr = Tcl_GetAssocData(interp, datakey, NULL);
    if (idataPtr == NULL) {
	return TCL_ERROR;
    }
    return GetHandle(idataPtr, id, handle, 0, NULL);
}


/*
 *----------------------------------------------------------------------
 *
 * NsDbAddCmds --
 *
 *      Add the nsdb commands.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

int
NsDbAddCmds(Tcl_Interp *interp, void *arg)
{
    InterpData *idataPtr;

    /*
     * Initialize the per-interp data.
     */

    idataPtr = ns_malloc(sizeof(InterpData));
    idataPtr->server = arg;
    idataPtr->interp = interp;
    idataPtr->cleanup = 0;
    Tcl_InitHashTable(&idataPtr->dbs, TCL_STRING_KEYS);
    Tcl_SetAssocData(interp, datakey, FreeData, idataPtr);

    Tcl_CreateObjCommand(interp, "ns_db", DbObjCmd, idataPtr, NULL);
    Tcl_CreateCommand(interp, "ns_quotelisttolist", QuoteListToListCmd, idataPtr, NULL);
    Tcl_CreateCommand(interp, "ns_getcsv", GetCsvCmd, idataPtr, NULL);
    Tcl_CreateCommand(interp, "ns_dberrorcode", DbErrorCodeCmd, idataPtr, NULL);
    Tcl_CreateCommand(interp, "ns_dberrormsg", DbErrorMsgCmd, idataPtr, NULL);
    Tcl_CreateCommand(interp, "ns_getcsv", GetCsvCmd, idataPtr, NULL);
    Tcl_CreateCommand(interp, "ns_dbconfigpath", DbConfigPathCmd, idataPtr, NULL);
    Tcl_CreateCommand(interp, "ns_pooldescription", PoolDescriptionCmd, idataPtr, NULL);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * DbCmd --
 *
 *      Implement the AOLserver ns_db Tcl command.
 *
 * Results:
 *      Return TCL_OK upon success and TCL_ERROR otherwise.
 *
 * Side effects:
 *      Depends on the command.
 *
 *----------------------------------------------------------------------
 */

static int
DbObjCmd(ClientData data, Tcl_Interp *interp, int objc, Tcl_Obj **objv)
{
#define MAXHANDLES 4
    InterpData	   *idataPtr = data;
    Ns_DbHandle    *handle, **handlesPtrPtr, *staticHandles[MAXHANDLES];
    Ns_Set         *row;
    Tcl_HashEntry  *hPtr;
    Tcl_Obj	   *resultPtr;
    char           *arg, *pool, buf[32];
    int		    timeout, nhandles, n, status;
    static CONST char *opts[] = {
	"getrow", "gethandle", "releasehandle", "select", "dml",
	"1row", "0or1row", "bindrow", "exec", "sp_exec", "sp_getparams",
	"sp_returncode", "sp_setparam", "sp_start", "exception",
	"flush", "bouncepool", "cancel", "connected", "datasource",
	"dbtype", "disconnect", "driver", "interpretsqlfile",
	"password", "poolname", "pools", "resethandle", "setexception",
	"user", "verbose", NULL
    }; enum {
	Db_getrowIdx, Db_gethandleIdx, Db_releasehandleIdx,
	Db_selectIdx, Db_dmlIdx, Db_1rowIdx, Db_0or1rowIdx,
	Db_bindrowIdx, Db_execIdx, Db_sp_execIdx, Db_sp_getparamsIdx,
	Db_sp_returncodeIdx, Db_sp_setparamIdx, Db_sp_startIdx,
	Db_exceptionIdx, Db_flushIdx, Db_bouncepoolIdx, Db_cancelIdx,
	Db_connectedIdx, Db_datasourceIdx, Db_dbtypeIdx, Db_disconnectIdx,
	Db_driverIdx, Db_interpretsqlfileIdx, Db_passwordIdx,
	Db_poolnameIdx, Db_poolsIdx, Db_resethandleIdx, Db_setexceptionIdx,
	Db_userIdx, Db_verboseIdx
    } opt;
    static CONST char *spopts[] = {
	"in", "out", NULL
    };
    enum {
	Sp_inIdx, Sp_outIdx
    } spopt;

    if (objc < 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "option ?args?");
        return TCL_ERROR;
    }
    if (Tcl_GetIndexFromObj(interp, objv[1], opts, "option", 1,
                (int *) &opt) != TCL_OK) {
        return TCL_ERROR;
    }

    resultPtr = Tcl_GetObjResult(interp);
    handle = NULL;
    status = NS_OK;

    switch (opt) {

    /*
     * The following options require just a db arg and clears
     * any exception when getting the handle.
     */

    case Db_bindrowIdx:
    case Db_cancelIdx:
    case Db_connectedIdx:
    case Db_datasourceIdx:
    case Db_dbtypeIdx:
    case Db_disconnectIdx:
    case Db_driverIdx:
    case Db_flushIdx:
    case Db_passwordIdx:
    case Db_poolnameIdx:
    case Db_releasehandleIdx:
    case Db_resethandleIdx:
    case Db_sp_execIdx:
    case Db_sp_getparamsIdx:
    case Db_sp_returncodeIdx:
    case Db_userIdx:
    	if (objc != 3) {
            Tcl_WrongNumArgs(interp, 2, objv, "dbId");
	    return 0;
    	}
    	if (GetHandleObj(idataPtr, objv[2], &handle, 1, &hPtr) != TCL_OK) {
	    return TCL_ERROR;
	}
	switch ((int) opt) {
    	case Db_bindrowIdx:
            row = Ns_DbBindRow(handle);
            EnterRow(interp, row, NS_TCL_SET_STATIC, &status);
	    break;

    	case Db_cancelIdx:
            status = Ns_DbCancel(handle);
	    break;

    	case Db_connectedIdx:
	    Tcl_SetBooleanObj(resultPtr, handle->connected);
	    break;

    	case Db_datasourceIdx:
	    Tcl_SetResult(interp, handle->datasource, TCL_STATIC);
	    break;

    	case Db_dbtypeIdx:
            Tcl_SetResult(interp, Ns_DbDriverDbType(handle), TCL_STATIC);
	    break;

    	case Db_disconnectIdx:
	    NsDbDisconnect(handle);
	    break;

    	case Db_driverIdx:
            Tcl_SetResult(interp, Ns_DbDriverName(handle), TCL_STATIC);
	    break;

    	case Db_flushIdx:
            status = Ns_DbFlush(handle);
	    break;

    	case Db_passwordIdx:
            Tcl_SetResult(interp, handle->password, TCL_VOLATILE);
	    break;

    	case Db_poolnameIdx:
            Tcl_SetResult(interp, handle->poolname, TCL_VOLATILE);
	    break;

    	case Db_releasehandleIdx:
	    Tcl_DeleteHashEntry(hPtr);
    	    Ns_DbPoolPutHandle(handle);
	    break;

    	case Db_resethandleIdx:
	    status = Ns_DbResetHandle(handle);
	    if (status == NS_OK) {
	    	Tcl_SetIntObj(resultPtr, NS_OK);
	    }
	    break;

    	case Db_sp_execIdx:
	    status = Ns_DbSpExec(handle);
	    if (status == NS_DML) {
		Tcl_SetResult(interp, "NS_DML", TCL_STATIC);
	    } else if (status == NS_ROWS) {
		Tcl_SetResult(interp, "NS_ROWS", TCL_STATIC);
	    }
	    break;

    	case Db_sp_getparamsIdx:
	    row = Ns_DbSpGetParams(handle);
	    EnterRow(interp, row, NS_TCL_SET_DYNAMIC, &status);
	    break;

    	case Db_sp_returncodeIdx:
	    status = Ns_DbSpReturnCode(handle, buf, 32);
	    if (status == NS_OK) {
		Tcl_SetResult(interp, buf, TCL_VOLATILE);
	    }
	    break;

    	case Db_userIdx:
            Tcl_SetResult(interp, handle->user, TCL_VOLATILE);
	    break;
	}
	break;

    /*
     * The following also requires just a db argument and
     * preserves any  exception.
     */

    case Db_exceptionIdx:
    	if (objc != 3) {
            Tcl_WrongNumArgs(interp, 2, objv, "dbId");
	    return 0;
    	}
    	if (GetHandleObj(idataPtr, objv[2], &handle, 0, &hPtr) != TCL_OK) {
	    return TCL_ERROR;
	}
        Tcl_AppendElement(interp, handle->cExceptionCode);
        Tcl_AppendElement(interp, handle->dsExceptionMsg.string);
	break;

    /*
     * The following options require a db and extra string arg and
     * clear any exception.
     */

    case Db_0or1rowIdx:
    case Db_1rowIdx:
    case Db_dmlIdx:
    case Db_execIdx:
    case Db_getrowIdx:
    case Db_interpretsqlfileIdx:
    case Db_selectIdx:
    case Db_sp_startIdx:
    	if (objc != 4) {
            Tcl_WrongNumArgs(interp, 2, objv, "dbId arg");
	    return 0;
    	}
    	if (GetHandleObj(idataPtr, objv[2], &handle, 1, &hPtr) != TCL_OK) {
	    return TCL_ERROR;
	}
	arg = Tcl_GetString(objv[3]);

	switch ((int) opt) {
    	case Db_0or1rowIdx:
            row = Ns_Db0or1Row(handle, arg, &n);
            if (row != NULL && n == 0) {
                Ns_SetFree(row);
	    } else {
		EnterRow(interp, row, NS_TCL_SET_DYNAMIC, &status);
	    }
	    break;

    	case Db_1rowIdx:
            row = Ns_Db1Row(handle, arg);
	    EnterRow(interp, row, NS_TCL_SET_DYNAMIC, &status);
	    break;

    	case Db_dmlIdx:
	    status = Ns_DbDML(handle, arg);
	    break;

        case Db_execIdx:
	    status = Ns_DbExec(handle, arg);
	    if (status == NS_DML) {
                Tcl_SetResult(interp, "NS_DML", TCL_STATIC);
	    } else if (status == NS_ROWS) {
                Tcl_SetResult(interp, "NS_ROWS", TCL_STATIC);
	    }
	    break;

    	case Db_getrowIdx:
            if (Ns_TclGetSet2(interp, arg, &row) != TCL_OK) {
                return TCL_ERROR;
            }
	    status = Ns_DbGetRow(handle, row);
	    if (status == NS_OK) {
		Tcl_SetBooleanObj(resultPtr, 1);
	    } else if (status == NS_END_DATA) {
		Tcl_SetBooleanObj(resultPtr, 0);
	    }
	    break;

    	case Db_interpretsqlfileIdx:
	    status = Ns_DbInterpretSqlFile(handle, arg);
	    break;

    	case Db_selectIdx:
            row = Ns_DbSelect(handle, arg);
            EnterRow(interp, row, NS_TCL_SET_STATIC, &status);
	    break;

    	case Db_sp_startIdx:
	    status = Ns_DbSpStart(handle, arg);
	    if (status == NS_OK) {
		Tcl_SetBooleanObj(resultPtr, 0);
	    }
	    break;
	}
	break;

    /*
     * The remaining options requiring specific args.
     */

    case Db_sp_setparamIdx:
	if (objc != 7) {
            Tcl_WrongNumArgs(interp, 2, objv,
			     "dbId paramname type in|out value");
	    return TCL_ERROR;
	}
    	if (Tcl_GetIndexFromObj(interp, objv[5], spopts, "option", 0,
                (int *) &spopt) != TCL_OK) {
            return TCL_ERROR;
        }
    	if (GetHandleObj(idataPtr, objv[2], &handle, 1, &hPtr) != TCL_OK) {
	    return TCL_ERROR;
	}
	status = Ns_DbSpSetParam(handle, Tcl_GetString(objv[3]),
				 Tcl_GetString(objv[4]),
				 spopts[spopt],
				 Tcl_GetString(objv[6]));
	if (status == NS_OK) {
	    Tcl_SetBooleanObj(resultPtr, 1);
	}
	break;

    case Db_gethandleIdx:
	timeout = -1;
	if (objc >= 4) {
	    arg = Tcl_GetString(objv[2]);
	    if (STREQ(arg, "-timeout")) {
		if (Tcl_GetIntFromObj(interp, objv[3], &timeout) != TCL_OK) {
		    return TCL_ERROR;
		}
		objv += 2;
		objc -= 2;
	    } else if (objc > 4) {
            	Tcl_WrongNumArgs(interp, 2, objv,
				 "?-timeout seconds? ?pool? ?nhandles?");
		return TCL_ERROR;
	    }
	}
	objv += 2;
	objc -= 2;

	/*
	 * Determine the pool and requested number of handles
	 * from the remaining args.
	 */
       
	if (objc > 0) {
	    pool = Tcl_GetString(objv[0]);
	} else {
	    pool = Ns_DbPoolDefault(idataPtr->server);
            if (pool == NULL) {
                Tcl_SetResult(interp, "no defaultpool configured", TCL_STATIC);
                return TCL_ERROR;
            }
        }
        if (Ns_DbPoolAllowable(idataPtr->server, pool) == NS_FALSE) {
            Tcl_AppendResult(interp, "no access to pool: \"", pool, "\"",
			     NULL);
            return TCL_ERROR;
        }
        if (objc < 2) {
	    nhandles = 1;
	} else {
            if (Tcl_GetIntFromObj(interp, objv[1], &nhandles) != TCL_OK) {
                return TCL_ERROR;
            }
	    if (nhandles <= 0) {
                Tcl_AppendResult(interp, "invalid nhandles:  \"",
		    Tcl_GetString(objv[1]),
                    "\": should be greater than 0.", NULL);
                return TCL_ERROR;
            }
	}

    	/*
         * Allocate handles and enter them into Tcl.
         */

	if (nhandles > MAXHANDLES) {
	    handlesPtrPtr = ns_malloc(nhandles * sizeof(Ns_DbHandle *));
	} else {
    	    handlesPtrPtr = staticHandles;
	}
	status = Ns_DbPoolTimedGetMultipleHandles(handlesPtrPtr, pool, 
    	    	                                  nhandles, timeout);
    	if (status == NS_OK) {
	    while (--nhandles >= 0) {
                EnterHandle(idataPtr, interp, handlesPtrPtr[nhandles]);
            }
	}
	if (handlesPtrPtr != staticHandles) {
	    ns_free(handlesPtrPtr);
	}
	if (status != NS_TIMEOUT && status != NS_OK) {
	    sprintf(buf, "%d", nhandles);
            Tcl_AppendResult(interp, "could not allocate ", buf,
		" handle(s) from pool \"", pool, "\"", NULL);
            return TCL_ERROR;
        }
	break;

    case Db_bouncepoolIdx:
	if (objc != 3) {
            Tcl_WrongNumArgs(interp, 2, objv, "pool");
	    return TCL_ERROR;
	}
	status = Ns_DbBouncePool(Tcl_GetString(objv[2]));
	break;

    case Db_poolsIdx:
        if (objc > 2) {
            Tcl_WrongNumArgs(interp, 2, objv, NULL);
            return TCL_ERROR;
        }
	pool = Ns_DbPoolList(idataPtr->server);
	if (pool != NULL) {
	    while (*pool != '\0') {
		Tcl_AppendElement(interp, pool);
		pool = pool + strlen(pool) + 1;
	    }
	}
	break;

    case Db_setexceptionIdx:
        if (objc != 5) {
            Tcl_WrongNumArgs(interp, 2, objv, "dbId code message");
            return TCL_ERROR;
        }
    	if (GetHandleObj(idataPtr, objv[2], &handle, 1, NULL) != TCL_OK) {
	    return TCL_ERROR;
	}
	arg = Tcl_GetStringFromObj(objv[3], &n);
	if (n > 5) {
            Tcl_AppendResult(interp, "code \"", arg,
		    "\" more than 5 characters", NULL);
            return TCL_ERROR;
        }
        Ns_DbSetException(handle, arg, Tcl_GetString(objv[4]));
	break;

    case Db_verboseIdx:
        if (objc != 3 && objc != 4) {
            Tcl_WrongNumArgs(interp, 2, objv, "dbId ?bool?");
	    return TCL_ERROR;
	}
    	if (GetHandleObj(idataPtr, objv[2], &handle, 1, NULL) != TCL_OK) {
	    return TCL_ERROR;
	}
        if (objc == 4) {
            if (Tcl_GetBooleanFromObj(interp, objv[3], &n) != TCL_OK) {
                return TCL_ERROR;
	    }
            handle->verbose = n;
        }
	Tcl_SetIntObj(resultPtr, handle->verbose);
	break;
    }

    if (status == NS_ERROR) {
    	Tcl_AppendResult(interp, "Database operation \"", opts[opt],
		"\" failed", NULL);
	if (handle != NULL && handle->cExceptionCode[0] != '\0') {
            Tcl_AppendResult(interp, " (exception ",
			     handle->cExceptionCode, NULL);
            if (handle->dsExceptionMsg.length > 0) {
            	Tcl_AppendResult(interp, ", \"",
				 handle->dsExceptionMsg.string, "\"", NULL);
	    }
            Tcl_AppendResult(interp, ")", NULL);
        }
	return TCL_ERROR;
    }

    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 * DbErrorCodeCmd --
 *
 *      Get database exception code for the database handle.
 *
 * Results:
 *      Returns TCL_OK and database exception code is set as Tcl result
 *	or TCL_ERROR if failure.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static int
ErrorCmd(ClientData arg, Tcl_Interp *interp, int argc, CONST char **argv, int cmd)
{
    InterpData *idataPtr = arg;
    Ns_DbHandle *handle;

    if (argc != 2) {
        Tcl_AppendResult(interp, "wrong # args:  should be \"",
            argv[0], " dbId\"", NULL);
        return TCL_ERROR;
    }
    if (GetHandle(idataPtr, (char*) argv[1], &handle, 0, NULL) != TCL_OK) {
        return TCL_ERROR;
    }
    if (cmd == 'c') {
    	Tcl_SetResult(interp, handle->cExceptionCode, TCL_VOLATILE);
    } else {
    	Tcl_SetResult(interp, handle->dsExceptionMsg.string, TCL_VOLATILE);
    }
    return TCL_OK;
}

static int
DbErrorCodeCmd(ClientData arg, Tcl_Interp *interp, int argc, CONST char **argv)
{
    return ErrorCmd(arg, interp, argc, argv, 'c');
}

static int
DbErrorMsgCmd(ClientData arg, Tcl_Interp *interp, int argc, CONST char **argv)
{
    return ErrorCmd(arg, interp, argc, argv, 'm');
}


/*
 *----------------------------------------------------------------------
 * DbConfigPathCmd --
 *
 *      Get the database section name from the configuration file.
 *
 * Results:
 *      TCL_OK and the database section name is set as the Tcl result
 *	or TCL_ERROR if failure.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static int
DbConfigPathCmd(ClientData arg, Tcl_Interp *interp, int argc, CONST char **argv)
{
    InterpData *idataPtr = arg;
    char *section;

    if (argc != 1) {
        Tcl_AppendResult(interp, "wrong # of args: should be \"", argv[0],
			 "\"", NULL);
        return TCL_ERROR;
    }
    section = Ns_ConfigGetPath(idataPtr->server, NULL, "db", NULL);
    Tcl_SetResult(interp, section, TCL_STATIC);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 * PoolDescriptionCmd --
 *
 *      Get the pool's description string.
 *
 * Results:
 *      Return TCL_OK and the pool's description string is set as the 
 *	Tcl result string or TCL_ERROR if failure.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static int
PoolDescriptionCmd(ClientData arg, Tcl_Interp *interp, int argc, 
        CONST char **argv)
{
    if (argc != 2) {
        Tcl_AppendResult(interp, "wrong # of args: should be \"", 
             (char*)argv[0], " poolname\"", NULL);
        return TCL_ERROR;
    }
    Tcl_SetResult(interp, Ns_DbPoolDescription((char*)argv[1]),TCL_STATIC);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 * QuoteListToListCmd --
 *
 *      Remove space, \ and ' characters in a string.
 *
 * Results:
 *      TCL_OK and set the stripped string as the Tcl result or TCL_ERROR
 *	if failure.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static int
QuoteListToListCmd(ClientData arg, Tcl_Interp *interp, int argc,
			CONST char **argv)
{
    char       *quotelist;
    int         inquotes;
    Ns_DString  ds;

    if (argc != 2) {
        Tcl_AppendResult(interp, "wrong # of args: should be \"",
	    argv[0], " quotelist\"", NULL);
        return TCL_ERROR;
    }
    quotelist = (char*)argv[1];
    inquotes = NS_FALSE;
    Ns_DStringInit(&ds);
    while (*quotelist != '\0') {
        if (isspace(UCHAR(*quotelist)) && inquotes == NS_FALSE) {
            if (ds.length != 0) {
                Tcl_AppendElement(interp, ds.string);
                Ns_DStringTrunc(&ds, 0);
            }
            while (isspace(UCHAR(*quotelist))) {
                quotelist++;
            }
        } else if (*quotelist == '\\' && (*(quotelist + 1) != '\0')) {
            Ns_DStringNAppend(&ds, quotelist + 1, 1);
            quotelist += 2;
        } else if (*quotelist == '\'') {
            if (inquotes) {
                /* Finish element */
                Tcl_AppendElement(interp, ds.string);
                Ns_DStringTrunc(&ds, 0);
                inquotes = NS_FALSE;
            } else {
                /* Start element */
                inquotes = NS_TRUE;
            }
            quotelist++;
        } else {
            Ns_DStringNAppend(&ds, quotelist, 1);
            quotelist++;
        }
    }
    if (ds.length != 0) {
        Tcl_AppendElement(interp, ds.string);
    }
    Ns_DStringFree(&ds);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * GetCsvCmd --
 *
 *	Implement the ns_getcvs command to read a line from a CSV file
 *	and parse the results into a Tcl list variable.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	One line is read for given open channel.
 *
 *----------------------------------------------------------------------
 */

static int
GetCsvCmd(ClientData arg, Tcl_Interp *interp, int argc, 
        CONST char **argv)
{
    int             ncols, inquote, quoted, blank;
    char            c, *p, buf[20];
    const char	   *result;
    Tcl_DString     line, cols, elem;
    Tcl_Channel	    chan;

    if (argc != 3) {
        Tcl_AppendResult(interp, "wrong # of args: should be \"",
	    argv[0], " fileId varName\"", NULL);
        return TCL_ERROR;
    }
    if (Ns_TclGetOpenChannel(interp, (char*)argv[1], 0, 0, &chan) == TCL_ERROR) {
        return TCL_ERROR;
    }
    
    Tcl_DStringInit(&line);
    if (Tcl_Gets(chan, &line) < 0) {
	Tcl_DStringFree(&line);
    	if (!Tcl_Eof(chan)) {
	    Tcl_AppendResult(interp, "could not read from ", argv[1],
	        ": ", Tcl_PosixError(interp), NULL);
	    return TCL_ERROR;
	}
	Tcl_SetResult(interp, "-1", TCL_STATIC);
	return TCL_OK;
    }

    Tcl_DStringInit(&cols);
    Tcl_DStringInit(&elem);
    ncols = 0;
    inquote = 0;
    quoted = 0;
    blank = 1;
    p = line.string;
    while (*p != '\0') {
        c = *p++;
loopstart:
        if (inquote) {
            if (c == '"') {
		c = *p++;
		if (c == '\0') {
		    break;
		}
                if (c == '"') {
                    Tcl_DStringAppend(&elem, &c, 1);
                } else {
                    inquote = 0;
                    goto loopstart;
                }
            } else {
                Tcl_DStringAppend(&elem, &c, 1);
            }
        } else {
            if ((c == '\n') || (c == '\r')) {
                while ((c = *p++) != '\0') {
                    if ((c != '\n') && (c != '\r')) {
			--p;
                        break;
                    }
                }
                break;
            }
            if (c == '"') {
                inquote = 1;
                quoted = 1;
                blank = 0;
            } else if ((c == '\r') || (elem.length == 0 && isspace(UCHAR(c)))) {
                continue;
            } else if (c == ',') {
                if (!quoted) {
                    Ns_StrTrimRight(elem.string);
                }
		Tcl_DStringAppendElement(&cols, elem.string);
                Tcl_DStringTrunc(&elem, 0);
                ncols++;
                quoted = 0;
            } else {
                blank = 0;
                Tcl_DStringAppend(&elem, &c, 1);
            }
        }
    }
    if (!quoted) {
        Ns_StrTrimRight(elem.string);
    }
    if (!blank) {
	Tcl_DStringAppendElement(&cols, elem.string);
        ncols++;
    }
    result = Tcl_SetVar(interp, argv[2], cols.string, TCL_LEAVE_ERR_MSG);
    Tcl_DStringFree(&line);
    Tcl_DStringFree(&cols);
    Tcl_DStringFree(&elem);
    if (result == NULL) {
	return TCL_ERROR;
    }
    sprintf(buf, "%d", ncols);
    Tcl_SetResult(interp, buf, TCL_VOLATILE);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 * GetHandle, GetHandleObj --
 *
 *      Get database handle from its handle id.
 *
 * Results:
 *      Return TCL_OK if handle is found or TCL_ERROR otherwise.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
GetHandle(InterpData *idataPtr, char *id, Ns_DbHandle **handlePtr,
	    int clear, Tcl_HashEntry **hPtrPtr)
{
    Ns_DbHandle *handle;
    Tcl_HashEntry  *hPtr;

    hPtr = Tcl_FindHashEntry(&idataPtr->dbs, id);
    if (hPtr == NULL) {
	Tcl_AppendResult(idataPtr->interp, "invalid database id:  \"", id,
			"\"", NULL);
	return TCL_ERROR;
    }
    handle = (Ns_DbHandle *) Tcl_GetHashValue(hPtr);
    if (clear) {
    	Ns_DStringFree(&handle->dsExceptionMsg);
    	handle->cExceptionCode[0] = '\0';
    }
    if (hPtrPtr != NULL) {
	*hPtrPtr = hPtr;
    }
    if (handlePtr != NULL) {
	*handlePtr = handle;
    }
    return TCL_OK;
}


static int
GetHandleObj(InterpData *idataPtr, Tcl_Obj *obj, Ns_DbHandle **handlePtr,
	    int clear, Tcl_HashEntry **hPtrPtr)
{
    return GetHandle(idataPtr, Tcl_GetString(obj), handlePtr, clear, hPtrPtr);
}


/*
 *----------------------------------------------------------------------
 * EnterHandle --
 *
 *      Enter a database handle and create its handle id.
 *
 * Results:
 *      The database handle id is returned as a Tcl result.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static void
EnterHandle(InterpData *idataPtr, Tcl_Interp *interp, Ns_DbHandle *handle)
{
    Tcl_HashEntry *hPtr;
    int            new, next;
    char	   buf[100];

    if (!idataPtr->cleanup) {
	Ns_TclRegisterDeferred(interp, ReleaseDbs, idataPtr);
	idataPtr->cleanup = 1;
    }
    next = idataPtr->dbs.numEntries;
    do {
        sprintf(buf, "nsdb%x", next++);
        hPtr = Tcl_CreateHashEntry(&idataPtr->dbs, buf, &new);
    } while (!new);
    Tcl_AppendElement(interp, buf);
    Tcl_SetHashValue(hPtr, handle);
}


/*
 *----------------------------------------------------------------------
 * FreeData --
 *
 *      Free per-interp data at interp delete time.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static void
FreeData(ClientData arg, Tcl_Interp *interp)
{
    InterpData *idataPtr = arg;

    Tcl_DeleteHashTable(&idataPtr->dbs);
    ns_free(idataPtr);
}


/*
 *----------------------------------------------------------------------
 * ReleaseDbs --
 *
 *      Release any database handles still held.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static void
ReleaseDbs(Tcl_Interp *interp, void *arg)
{
    Ns_DbHandle *handle;
    Tcl_HashEntry  *hPtr;
    Tcl_HashSearch  search;
    InterpData *idataPtr = arg;

    hPtr = Tcl_FirstHashEntry(&idataPtr->dbs, &search);
    while (hPtr != NULL) {
    	handle = Tcl_GetHashValue(hPtr);
   	Ns_DbPoolPutHandle(handle);
    	hPtr = Tcl_NextHashEntry(&search);
    }
    Tcl_DeleteHashTable(&idataPtr->dbs);
    Tcl_InitHashTable(&idataPtr->dbs, TCL_STRING_KEYS);
    idataPtr->cleanup = 0;
}


/*
 *----------------------------------------------------------------------
 * EnterRow --
 *
 *      Enter a db row set into interp.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      Will set *statusPtr to NS_ERROR if row is null.
 *
 *----------------------------------------------------------------------
 */

static void
EnterRow(Tcl_Interp *interp, Ns_Set *row, int flags, int *statusPtr)
{
    if (row == NULL) {
	*statusPtr = NS_ERROR;
    } else {
        Ns_TclEnterSet(interp, row, flags);
	*statusPtr = NS_OK;
    }
}
