/* FreeTDS - Library of routines accessing Sybase and Microsoft databases
 * Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005  Brian Bruns
 * Copyright (C) 2006-2015  Frediano Ziglio
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <config.h>

#include <stdarg.h>

#include <freetds/time.h>

#include <assert.h>
#include <stdio.h>

#if HAVE_STDLIB_H
#include <stdlib.h>
#endif /* HAVE_STDLIB_H */

#if HAVE_STRING_H
#include <string.h>
#endif /* HAVE_STRING_H */

#if HAVE_UNISTD_H
#include <unistd.h>
#endif /* HAVE_UNISTD_H */

#if HAVE_ERRNO_H
# include <errno.h>
#endif /* HAVE_ERRNO_H */

/** 
 * \ingroup dblib_core
 * \remarks Either SYBDBLIB or MSDBLIB (not both) must be defined. 
 * 	This affects how certain application-addressable 
 * 	strucures are defined.  
 */
#include <freetds/tds.h>
#include <freetds/thread.h>
#include <freetds/convert.h>
#include <freetds/string.h>
#include <freetds/data.h>
#include <replacements.h>
#include <sybfront.h>
#include <sybdb.h>
#include <syberror.h>
#include <dblib.h>

static RETCODE _dbresults(DBPROCESS * dbproc);
static BYTE *_dbcoldata(TDSCOLUMN *colinfo);
static int _get_printable_size(TDSCOLUMN * colinfo);
static char *_dbprdate(char *timestr);
static int _dbnullable(DBPROCESS * dbproc, int column);
static const char *tds_prdatatype(int datatype_token);

static int default_err_handler(DBPROCESS * dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr);

void copy_data_to_host_var(DBPROCESS *, TDS_SERVER_TYPE, const BYTE *, int, BYTE *, DBINT, int, DBINT *);
RETCODE dbgetnull(DBPROCESS *dbproc, int bindtype, int varlen, BYTE* varaddr);

/**
 * \file dblib.c
 * Main implementation file for \c db-lib.
 */
/**
 * \file bcp.c
 * Implementation of \c db-lib bulk copy functions.
 */
/**
 * \defgroup dblib_api The db-lib API
 * Functions callable by \c db-lib client programs
 *
 * The \c db_lib interface is implemented by both Sybase and Microsoft.  FreeTDS seeks to implement 
 * first the intersection of the functions defined by the vendors.  
 */
 
/**
 * \ingroup dblib_api
 * \defgroup dblib_core Primary functions
 * Core functions needed by most db-lib programs.  
*/
/**
 * \ingroup dblib_api
 * \defgroup dblib_rpc Remote Procedure functions
 * Functions used with stored procedures.  
 * Especially useful for OUTPUT parameters, because modern Microsoft servers do not 
 * return output parameter data to the client unless the procedure was invoked
 * with dbrpcsend().  
 */
/**
 * \ingroup dblib_api
 * \defgroup dblib_bcp Bulk copy functions
 * Functions to bulk-copy (a/k/a \em bcp) data to/from the database.  
 */
/**
 * \ingroup dblib_bcp
 * \defgroup dblib_bcp_internal Internal bcp functions
 * Static functions internal to the bcp library.  
 */
/**
 * \ingroup dblib_api
 * \defgroup dblib_money Money functions 
 * Functions to manipulate the MONEY datatype.  
 */
/**
 * \ingroup dblib_api
 * \defgroup dblib_datetime Datetime functions 
 * Functions to manipulate DBDATETIME structures.  Defined by Sybase only.  
 * These are not implemented:
 *	- dbdate4cmp()
 *	- dbdate4zero()
 *	- dbdatechar()
 *	- dbdatename()
 *	- dbdateorder()
 *	- dbdatepart()
 *	- dbdatezero()
 *	- dbdayname()
 */
/**
 * \ingroup dblib_api
 * \defgroup dblib_internal Internals
 * Functions called within \c db-lib for self-help.  
 * These functions are of interest only to people hacking on the FreeTDS db-lib implementation.  
 */
/**
 * \ingroup dblib_api
 * \defgroup dblib_unimplemented Unimplemented
 * Functions thus far not implemented in the FreeTDS db-lib implementation.  
 * While some of these are simply awaiting someone with time and skill (and inclination)
 * it might be noted here that the old browse functions (e.g. dbcolbrowse()) 
 * are on the never-to-do list.  
 * They were defined by Sybase and were superseded long ago, although they're still
 * present in Microsoft's implementation.  
 * They were never popular and today better alternatives are available.  
 * For completeness, they are:
 * 	- dbcolbrowse()
 * 	- dbcolsource()
 * 	- dbfreequal()
 * 	- dbqual()
 * 	- dbtabbrowse()
 * 	- dbtabcount()
 * 	- dbtabname()
 * 	- dbtabsource()
 * 	- dbtsnewlen()
 * 	- dbtsnewval()
 * 	- dbtsput()
 */

/* info/err message handler functions (or rather pointers to them) */
MHANDLEFUNC _dblib_msg_handler = NULL;
EHANDLEFUNC _dblib_err_handler = default_err_handler;

/** \internal
 * \dblib_internal
 * \remarks A db-lib connection has an implicit TDS context. 
 */
typedef struct dblib_context
{
	/** reference count, time dbinit called */
	int ref_count;

	/** libTDS context */
	TDSCONTEXT *tds_ctx;
	/** libTDS context reference counter */
	int tds_ctx_ref_count;

	/* save all connection in a list */
	TDSSOCKET **connection_list;
	int connection_list_size;
	int connection_list_size_represented;
	char *recftos_filename;
	int recftos_filenum;
	int login_timeout;	/**< not used unless positive */
	int query_timeout;	/**< not used unless positive */
}
DBLIBCONTEXT;

static DBLIBCONTEXT g_dblib_ctx;
static tds_mutex dblib_mutex = TDS_MUTEX_INITIALIZER;

static int g_dblib_version =
#ifdef TDS42
	DBVERSION_42;
#elif TDS50
	DBVERSION_100;
#elif TDS46
	DBVERSION_46;
#elif TDS70
	DBVERSION_70;
#elif TDS71
	DBVERSION_71;
#elif TDS72
	DBVERSION_72;
#elif TDS73
	DBVERSION_73;
#elif TDS74
	DBVERSION_74;
#else
	DBVERSION_UNKNOWN;
#endif


static int
dblib_add_connection(DBLIBCONTEXT * ctx, TDSSOCKET * tds)
{
	int i = 0;
	const int list_size = ctx->connection_list_size_represented;

	tdsdump_log(TDS_DBG_FUNC, "dblib_add_connection(%p, %p)\n", ctx, tds);

	while (i < list_size && ctx->connection_list[i])
		i++;
	if (i == list_size) {
		fprintf(stderr, "Max connections reached, increase value of TDS_MAX_CONN\n");
		return 1;
	} else {
		ctx->connection_list[i] = tds;
		return 0;
	}
}

static void
dblib_del_connection(DBLIBCONTEXT * ctx, TDSSOCKET * tds)
{
	int i = 0;
	const int list_size = ctx->connection_list_size;

	tdsdump_log(TDS_DBG_FUNC, "dblib_del_connection(%p, %p)\n", ctx, tds);

	while (i < list_size && ctx->connection_list[i] != tds)
		i++;
	if (i == list_size) {
		/* connection wasn't on the free list...now what */
	} else {
		/* remove it */
		ctx->connection_list[i] = NULL;
	}
}

static TDSCONTEXT*
dblib_get_tds_ctx(void)
{
	tdsdump_log(TDS_DBG_FUNC, "dblib_get_tds_ctx(void)\n");

	tds_mutex_lock(&dblib_mutex);
	++g_dblib_ctx.tds_ctx_ref_count;
	if (g_dblib_ctx.tds_ctx == NULL) {
		g_dblib_ctx.tds_ctx = tds_alloc_context(&g_dblib_ctx);

		/*
		 * Set the functions in the TDS layer to point to the correct handler functions
		 */
		g_dblib_ctx.tds_ctx->msg_handler = _dblib_handle_info_message;
		g_dblib_ctx.tds_ctx->err_handler = _dblib_handle_err_message;
		g_dblib_ctx.tds_ctx->int_handler = _dblib_check_and_handle_interrupt;

		if (g_dblib_ctx.tds_ctx->locale && !g_dblib_ctx.tds_ctx->locale->date_fmt) {
			/* set default in case there's no locale file */
			const static char date_format[] = 
#ifndef _WIN32
							   "%b %e %Y %I:%M:%S:%z%p";
#else
							   "%b %d %Y %I:%M:%S:%z%p";
#endif
			g_dblib_ctx.tds_ctx->locale->date_fmt = strdup(date_format);
		}
	}
	tds_mutex_unlock(&dblib_mutex);
	return g_dblib_ctx.tds_ctx;
}

static void
dblib_release_tds_ctx(int count)
{
	tdsdump_log(TDS_DBG_FUNC, "dblib_release_tds_ctx(%d)\n", count);

	tds_mutex_lock(&dblib_mutex);
	g_dblib_ctx.tds_ctx_ref_count -= count;
	if (g_dblib_ctx.tds_ctx_ref_count <= 0) {
		tds_free_context(g_dblib_ctx.tds_ctx);
		g_dblib_ctx.tds_ctx = NULL;
	}
	tds_mutex_unlock(&dblib_mutex);
}

#include "buffering.h"

static void
db_env_chg(TDSSOCKET * tds, int type, char *oldval, char *newval)
{
	DBPROCESS *dbproc;

	assert(oldval != NULL && newval != NULL);
	if (strlen(oldval) == 1 && *oldval == 1)
		oldval = "(0x1)";
		
	tdsdump_log(TDS_DBG_FUNC, "db_env_chg(%p, %d, %s, %s)\n", tds, type, oldval, newval);

	if (!tds || !tds_get_parent(tds))
		return;
	dbproc = (DBPROCESS *) tds_get_parent(tds);

	dbproc->envchange_rcv |= (1 << (type - 1));
	switch (type) {
	case TDS_ENV_DATABASE:
		strlcpy(dbproc->dbcurdb, newval, sizeof(dbproc->dbcurdb));
		break;
	case TDS_ENV_CHARSET:
		strlcpy(dbproc->servcharset, newval, sizeof(dbproc->servcharset));
		break;
	default:
		break;
	}
	return;
}

/** \internal
 * \ingroup dblib_internal
 * \brief Sanity checks for column-oriented functions.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param pcolinfo address of pointer to a TDSCOLUMN structure.
 * \remarks Makes sure dbproc and the requested column are valid.  
 *	Calls dbperror() if not.  
 * \returns appropriate error or SUCCEED
 */
static TDSCOLUMN*
dbcolptr(DBPROCESS* dbproc, int column)
{
	if (!dbproc) {
		dbperror(dbproc, SYBENULL, 0);
		return NULL;
	}
	if (IS_TDSDEAD(dbproc->tds_socket)) {
		dbperror(dbproc, SYBEDDNE, 0);
		return NULL;
	}
	if (!dbproc->tds_socket->res_info)
		return NULL;
	if (column < 1 || column > dbproc->tds_socket->res_info->num_cols) {
		dbperror(dbproc, SYBECNOR, 0);
		return NULL;
	} 
	
	return dbproc->tds_socket->res_info->columns[column - 1];
}

static TDSCOLUMN*
dbacolptr(DBPROCESS* dbproc, int computeid, int column, int is_bind)
{
	int i;
	TDSSOCKET *tds;
	TDSCOMPUTEINFO *info;

	if (!dbproc) {
		dbperror(dbproc, SYBENULL, 0);
		return NULL;
	}
	tds = dbproc->tds_socket;
	if (IS_TDSDEAD(tds)) {
		dbperror(dbproc, SYBEDDNE, 0);
		return NULL;
	}
	for (i = 0;; ++i) {
		if (i >= tds->num_comp_info) {
			/* Attempt to bind user variable to a non-existent compute row */
			if (is_bind)
				dbperror(dbproc, SYBEBNCR, 0);
			return NULL;
		}
		info = tds->comp_info[i];
		if (info->computeid == computeid)
			break;
	}
	/* Fail if either the compute id or the column number is invalid. */
	if (column < 1 || column > info->num_cols) {
		dbperror(dbproc, is_bind ? SYBEABNC : SYBECNOR, 0);
		return NULL;
	}

	return info->columns[column - 1];
}

/*
 * Default null substitution values
 * Binding Type		Null Substitution Value
 * TINYBIND		0
 * SMALLBIND		0
 * INTBIND		0
 * CHARBIND		Empty string (padded with blanks)
 * STRINGBIND		Empty string (padded with blanks, null-terminated)
 * NTBSTRINGBIND	Empty string (null-terminated)
 * VARYCHARBIND		Empty string
 * BINARYBIND		Empty array (padded with zeros)
 * VARYBINBIND		Empty array
 * DATETIMEBIND		8 bytes of zeros
 * SMALLDATETIMEBIND	8 bytes of zeros
 * MONEYBIND		$0.00
 * SMALLMONEYBIND	$0.00
 * FLT8BIND		0.0
 * REALBIND		0.0
 * DECIMALBIND		0.0 (with default scale and precision)
 * NUMERICBIND		0.0 (with default scale and precision)
 * BOUNDARYBIND		Empty string (null-terminated)
 * SENSITIVITYBIND	Empty string (null-terminated)
 */

static const DBBIT		null_BIT = 0;
static const DBTINYINT		null_TINYINT = 0;
static const DBSMALLINT		null_SMALLINT = 0;
static const DBINT		null_INT = 0;
static const DBBIGINT		null_BIGINT = 0;
static const DBFLT8		null_FLT8 = 0;
static const DBREAL		null_REAL = 0;

static const DBCHAR		null_CHAR = '\0';
static const DBVARYCHAR		null_VARYCHAR = { 0, {0} };
static const DBBINARY		null_BINARY = 0;

static const DBDATETIME		null_DATETIME = { 0, 0 };
static const DBDATETIME4	null_SMALLDATETIME = { 0, 0 };
static const DBMONEY		null_MONEY = { 0, 0 };
static const DBMONEY4		null_SMALLMONEY = {0};
static const DBNUMERIC		null_NUMERIC = { 0, 0, {0} };
static const TDS_DATETIMEALL	null_DATETIMEALL = { 0, 0, 0, 0 };

static NULLREP default_null_representations[MAXBINDTYPES] = {
	/* CHARBIND	     0  */	  {         NULL, 0 }
	/* STRINGBIND	     1  */	, {         NULL, 0 }
	/* NTBSTRINGBIND     2  */	, { (BYTE*) &null_CHAR, sizeof(null_CHAR) }
	/* VARYCHARBIND      3  */	, { (BYTE*) &null_VARYCHAR, sizeof(null_VARYCHAR) }
	/* VARYBINBIND       4  */	, { (BYTE*) &null_VARYCHAR, sizeof(null_VARYCHAR) }
	/* no such bind      5  */	, {         NULL, 0 }			
	/* TINYBIND	     6  */	, {         &null_TINYINT, sizeof(null_TINYINT) }
	/* SMALLBIND	     7  */	, { (BYTE*) &null_SMALLINT, sizeof(null_SMALLINT) }
	/* INTBIND	     8  */	, { (BYTE*) &null_INT, sizeof(null_INT) }
	/* FLT8BIND	     9  */	, { (BYTE*) &null_FLT8, sizeof(null_FLT8) }
	/* REALBIND	     10 */	, { (BYTE*) &null_REAL, sizeof(null_REAL) }
	/* DATETIMEBIND      11 */	, { (BYTE*) &null_DATETIME, sizeof(null_DATETIME) }
	/* SMALLDATETIMEBIND 12 */	, { (BYTE*) &null_SMALLDATETIME, sizeof(null_SMALLDATETIME) }
	/* MONEYBIND	     13 */	, { (BYTE*) &null_MONEY, sizeof(null_MONEY) }
	/* SMALLMONEYBIND    14 */	, { (BYTE*) &null_SMALLMONEY, sizeof(null_SMALLMONEY) }
	/* BINARYBIND	     15 */	, {         NULL, 0 }		    
	/* BITBIND	     16 */	, {         &null_BIT, sizeof(null_BIT) }
	/* NUMERICBIND       17 */	, { (BYTE*) &null_NUMERIC, sizeof(null_NUMERIC) }
	/* DECIMALBIND       18 */	, { (BYTE*) &null_NUMERIC, sizeof(null_NUMERIC) }
	/* SRCNUMERICBIND    19 */	, { (BYTE*) &null_NUMERIC, sizeof(null_NUMERIC) }
	/* SRCDECIMALBIND    20 */	, { (BYTE*) &null_NUMERIC, sizeof(null_NUMERIC) }
	/* DATEBIND          21 */	, { (BYTE*) &null_INT, sizeof(null_INT) }
	/* TIMEBIND          22 */	, { (BYTE*) &null_INT, sizeof(null_INT) }
	/* BIGDATETIMEBIND   23 */	, { (BYTE*) &null_BIGINT, sizeof(null_BIGINT) }
	/* BIGTIMEBIND       24 */	, { (BYTE*) &null_BIGINT, sizeof(null_BIGINT) }
	/* 	             25 */	, {         NULL, 0 }
	/* 	             26 */	, {         NULL, 0 }
	/* 	             27 */	, {         NULL, 0 }
	/* 	             28 */	, {         NULL, 0 }
	/* 	             29 */	, {         NULL, 0 }
	/* BIGINTBIND        30 */	, { (BYTE*) &null_BIGINT, sizeof(null_BIGINT) }
	/* DATETIME2BIND     31 */	, { (BYTE*) &null_DATETIMEALL, sizeof(null_DATETIMEALL) }
	/* MAXBINDTYPES      32 */
};

static int
dbbindtype(int datatype)
{
	switch (datatype) {
	case SYBIMAGE:
	case SYBVARBINARY:
	case SYBBINARY:		return BINARYBIND;
	
	case SYBBIT:		return BITBIND;

	case SYBTEXT:
	case SYBVARCHAR:
	case SYBCHAR:		return NTBSTRINGBIND;
	
	case SYBDATETIME:	return DATETIMEBIND;
	case SYBDATETIME4:	return SMALLDATETIMEBIND;

	case SYBDATE:		return DATEBIND;
	case SYBTIME:		return TIMEBIND;

	case SYB5BIGDATETIME:	return BIGDATETIMEBIND;
	case SYB5BIGTIME:	return BIGTIMEBIND;

	case SYBDECIMAL:	return DECIMALBIND;
	case SYBNUMERIC:	return NUMERICBIND;
	
	case SYBFLT8:		return FLT8BIND;
	case SYBREAL:		return REALBIND;

	case SYBINT1:		return TINYBIND;
	case SYBINT2:		return SMALLBIND;
	case SYBINT4:		return INTBIND;
	case SYBINT8:		return BIGINTBIND;

	case SYBMONEY:		return MONEYBIND;
	case SYBMONEY4:		return SMALLMONEYBIND;

	case SYBMSDATE:
	case SYBMSTIME:
	case SYBMSDATETIME2:
	case SYBMSDATETIMEOFFSET:
				return DATETIME2BIND;

	default:
		assert(0 == "no such datatype");
	}
	
	return 0;
}

/** \internal
 * dbbind() says: "Note that if varlen is 0, no padding takes place"
 * dbgetnull() will not pad varaddr unless varlen is positive.  
 * Vartype              Program Type    Padding         Terminator
 * -------------------  --------------  --------------  ----------
 * CHARBIND             DBCHAR          blanks          none
 * STRINGBIND           DBCHAR          blanks          \0
 * NTBSTRINGBIND        DBCHAR          none            \0
 * VARYCHARBIND         DBVARYCHAR      none            none
 * BOUNDARYBIND         DBCHAR          none            \0
 * SENSITIVITYBIND      DBCHAR          none            \0
 */
RETCODE
dbgetnull(DBPROCESS *dbproc, int bindtype, int varlen, BYTE* varaddr)
{
	NULLREP *pnullrep = default_null_representations + bindtype;

	tdsdump_log(TDS_DBG_FUNC, "dbgetnull(%p, %d, %d, %p)\n", dbproc, bindtype, varlen, varaddr);

	CHECK_PARAMETER(varaddr, SYBENULL, FAIL);
	CHECK_PARAMETER(0 <= bindtype && bindtype < MAXBINDTYPES, SYBEBTYP, FAIL);

	/* dbproc can be NULL */
	if (NULL != dbproc) {
		assert(dbproc->nullreps);
		pnullrep = dbproc->nullreps + bindtype;
	}
	
	/* 
	 * Fixed types: ignore varlen
	 * Other types: ignore varlen if <= 0, else varlen must be >= pnullrep->len.
	 */
	switch (bindtype) {
	case DATETIMEBIND:
	case DATETIME2BIND:
	case DECIMALBIND:
	case SRCDECIMALBIND:
	case FLT8BIND:
	case INTBIND:
	case MONEYBIND:
	case NUMERICBIND:
	case SRCNUMERICBIND:
	case REALBIND:
	case SMALLBIND:
	case SMALLDATETIMEBIND:
	case SMALLMONEYBIND:
	case TINYBIND:
	case BIGINTBIND:
	case BITBIND:
	case TIMEBIND:
	case DATEBIND:
	case BIGDATETIMEBIND:
	case BIGTIMEBIND:
		memcpy(varaddr, pnullrep->bindval, pnullrep->len);
		return SUCCEED;
	case CHARBIND:
	case STRINGBIND:
	case NTBSTRINGBIND:
	case BINARYBIND:
	case VARYCHARBIND:
	case VARYBINBIND:
		if (pnullrep->bindval && (varlen <= 0 || (size_t)varlen >= pnullrep->len)) {
			memcpy(varaddr, pnullrep->bindval, pnullrep->len);
		}
		break;
	default:
		dbperror(dbproc, SYBEBTYP, 0);
		return FAIL;
	}

	/* 
	 * For variable-length types, nonpositive varlen indicates 
	 * buffer is "big enough" but also not to pad.
	 * Apply terminator (if applicable) and go home.  
	 */
	if (varlen <= 0) {
		varlen = pnullrep->len;
		switch (bindtype) {
		case STRINGBIND:
		case NTBSTRINGBIND:
			++varlen;
			break;
#if 0
		case BOUNDARYBIND:
		case SENSITIVITYBIND:
#endif
		}
	}
	
	if (varlen < (long)pnullrep->len) {
		tdsdump_log(TDS_DBG_FUNC, "dbgetnull: error: not setting varaddr(%p) because %d < %lu\n",
					varaddr, varlen, (unsigned long int) pnullrep->len);
		return FAIL;
	} 
		
	tdsdump_log(TDS_DBG_FUNC, "varaddr(%p) varlen %d < %lu?\n",
				varaddr, varlen, (unsigned long int) pnullrep->len);

	assert(varlen >= 0);

	/*
	 * CHARBIND		Empty string (padded with blanks)
	 * STRINGBIND		Empty string (padded with blanks, null-terminated)
	 * NTBSTRINGBIND	Empty string (unpadded, null-terminated)
	 * BINARYBIND		Empty array (padded with zeros)
	 */
	varaddr += pnullrep->len;
	varlen  -= (int)pnullrep->len;
	if (varlen > 0) {
		switch (bindtype) {
		case CHARBIND:
			memset(varaddr, ' ', varlen);
			break;
		case STRINGBIND:
			memset(varaddr, ' ', varlen);
			varaddr[varlen-1] = '\0';
			break;
		case NTBSTRINGBIND:
			varaddr[0] = '\0';
			break;
		case BINARYBIND:
			memset(varaddr, 0, varlen);
			break;
		case VARYCHARBIND:
		case VARYBINBIND:
			break;
		default:
			assert(!"unknown bindtype");
		}
	}	
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Initialize db-lib.  
 *
 * \remarks Call this function before trying to use db-lib in any way.  
 * Allocates various internal structures and reads \c locales.conf (if any) to determine the default
 * date format.  
 * \retval SUCCEED normal.  
 * \retval FAIL cannot allocate an array of \c TDS_MAX_CONN \c TDSSOCKET pointers.  
 */
RETCODE
dbinit(void)
{
	_dblib_err_handler = default_err_handler;

	tds_mutex_lock(&dblib_mutex);

	tdsdump_log(TDS_DBG_FUNC, "dbinit(void)\n");

	if (++g_dblib_ctx.ref_count != 1) {
		tds_mutex_unlock(&dblib_mutex);
		return SUCCEED;
	}
	/* 
	 * DBLIBCONTEXT stores a list of current connections so they may be closed with dbexit() 
	 */

	g_dblib_ctx.connection_list = tds_new0(TDSSOCKET *, TDS_MAX_CONN);
	if (g_dblib_ctx.connection_list == NULL) {
		tdsdump_log(TDS_DBG_FUNC, "dbinit: out of memory\n");
		tds_mutex_unlock(&dblib_mutex);
		return FAIL;
	}
	g_dblib_ctx.connection_list_size = TDS_MAX_CONN;
	g_dblib_ctx.connection_list_size_represented = TDS_MAX_CONN;

	g_dblib_ctx.login_timeout = -1;
	g_dblib_ctx.query_timeout = -1;

	tds_mutex_unlock(&dblib_mutex);

	dblib_get_tds_ctx();

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Allocate a \c LOGINREC structure.  
 *
 * \remarks A \c LOGINREC structure is passed to \c dbopen() to create a connection to the database. 
 * 	Does not communicate to the server; interacts strictly with library.  
 * \retval NULL the \c LOGINREC cannot be allocated.
 * \retval LOGINREC* to valid memory, otherwise.  
 */
LOGINREC *
dblogin(void)
{
	LOGINREC *loginrec;

	tdsdump_log(TDS_DBG_FUNC, "dblogin(void)\n");

	if ((loginrec = tds_new(LOGINREC, 1)) == NULL) {
		dbperror(NULL, SYBEMEM, errno);
		return NULL;
	}
	if ((loginrec->tds_login = tds_alloc_login(1)) == NULL) {
		dbperror(NULL, SYBEMEM, errno);
		free(loginrec);
		return NULL;
	}

	/* set default values for loginrec */
	if (!tds_set_library(loginrec->tds_login, "DB-Library")) {
		dbperror(NULL, SYBEMEM, errno);
		free(loginrec);
		return NULL;
	}

	return loginrec;
}

/**
 * \ingroup dblib_core
 * \brief free the \c LOGINREC
 *
 */
void
dbloginfree(LOGINREC * login)
{
	tdsdump_log(TDS_DBG_FUNC, "dbloginfree(%p)\n", login);

	if (login) {
		tds_free_login(login->tds_login);
		TDS_ZERO_FREE(login);
	}
}

/** \internal
 * \ingroup dblib_internal 
 * \brief Set the value of a string in a \c LOGINREC structure.  
 *
 * Called by various macros to populate \a login.  
 * \param login the \c LOGINREC* to modify.
 * \param value the value to set it to.  
 * \param which the field to set.  
 * \retval SUCCEED the value was set.
 * \retval FAIL \c DBSETHID or other invalid \a which was tried.  
 */
RETCODE
dbsetlname(LOGINREC * login, const char *value, int which)
{
	bool copy_ret;
	const char *value_nonull = value ? value : "";

	tdsdump_log(TDS_DBG_FUNC, "dbsetlname(%p, %s, %d)\n", login, value, which);

	if (login == NULL) {
		dbperror(NULL, SYBEASNL, 0);
		return FAIL;
	}

	if (TDS_MAX_LOGIN_STR_SZ < strlen(value_nonull)) {
		dbperror(NULL, SYBENTLL, 0);
		return FAIL;
	}

	switch (which) {
	case DBSETHOST:
		copy_ret = tds_set_host(login->tds_login, value_nonull);
		break;
	case DBSETUSER:
		copy_ret = tds_set_user(login->tds_login, value_nonull);
		break;
	case DBSETPWD:
		copy_ret = tds_set_passwd(login->tds_login, value_nonull);
		break;
	case DBSETAPP:
		copy_ret = tds_set_app(login->tds_login, value_nonull);
		break;
	case DBSETCHARSET:
		/* TODO NULL == no conversion desired */
		copy_ret = tds_set_client_charset(login->tds_login, value_nonull);
		break;
	case DBSETNATLANG:
		copy_ret = tds_set_language(login->tds_login, value_nonull);
		break;
	case DBSETDBNAME:
		copy_ret = !!tds_dstr_copy(&login->tds_login->database, value_nonull);
		break;
	default:
		dbperror(NULL, SYBEASUL, 0); /* Attempt to set unknown LOGINREC field */
		return FAIL;
		break;
	}

	if (!copy_ret)
		return FAIL;
	return SUCCEED;
}

/** \internal
 * \ingroup dblib_internal
 * \brief Set an integer value in a \c LOGINREC structure.  
 *
 * Called by various macros to populate \a login.  
 * \param login the \c LOGINREC* to modify.
 * \param value the value to set it to.  
 * \param which the field to set.  
 * \retval SUCCEED the value was set.
 * \retval FAIL anything other than \c DBSETPACKET was passed for \a which.  
 */
RETCODE
dbsetllong(LOGINREC * login, long value, int which)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetllong(%p, %ld, %d)\n", login, value, which);

	if( login == NULL ) {
		dbperror(NULL, SYBEASNL, 0);
		return FAIL;
	}

	switch (which) {
	case DBSETPACKET:
		if (0 <= value && value <= 999999) { 
			tds_set_packet(login->tds_login, value);
			return SUCCEED;
		}
		dbperror(0, SYBEBADPK, 0, (int) value, (int) login->tds_login->block_size);
		return FAIL;
		break;
	default:
		tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbsetllong() which = %d\n", which);
		return FAIL;
		break;
	}
}

#if defined(DBLIB_UNIMPLEMENTED)
/** \internal
 * \ingroup dblib_internal
 * \brief Set an integer value in a \c LOGINREC structure.  
 *
 * Called by various macros to populate \a login.  
 * \param login the \c LOGINREC* to modify.
 * \param value the value to set it to.  
 * \param which the field to set.  
 * \retval SUCCEED the value was set.
 * \retval FAIL anything other than \c DBSETHIER was passed for \a which.  
 */
RETCODE
dbsetlshort(LOGINREC * login, int value, int which)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetlshort(%p, %d, %d)\n", login, value, which);

	if( login == NULL ) {
		dbperror(NULL, SYBEASNL, 0);
		return FAIL;
	}

	switch (which) {
	case DBSETHIER:
	default:
		tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbsetlshort() which = %d\n", which);
		return FAIL;
		break;
	}
}
#endif

/** \internal
 * \ingroup dblib_internal
 * \brief Set a boolean value in a \c LOGINREC structure.  
 *
 * Called by various macros to populate \a login.  
 * \param login the \c LOGINREC* to modify.
 * \param value the value to set it to.  
 * \param which the field to set. 
 * \remark Only DBSETBCP is implemented.  
 * \retval SUCCEED the value was set.
 * \retval FAIL invalid value passed for \a which.  
 * \todo DBSETNOSHORT, DBSETENCRYPT, DBSETLABELED
 */
RETCODE
dbsetlbool(LOGINREC * login, int value, int which)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetlbool(%p, %d, %d)\n", login, value, which);

	if( login == NULL ) {
		dbperror(NULL, SYBEASNL, 0);
		return FAIL;
	}

	switch (which) {
	case DBSETBCP:
		tds_set_bulk(login->tds_login, (TDS_TINYINT) value);
		return SUCCEED;
		break;
	case DBSETUTF16:
		login->tds_login->use_utf16 = (value != 0);
		return SUCCEED;
	case DBSETNTLMV2:
		login->tds_login->use_ntlmv2 = (value != 0);
		return SUCCEED;
	case DBSETREADONLY:
		login->tds_login->readonly_intent = (value != 0);
		return SUCCEED;
	case DBSETENCRYPT:
	case DBSETLABELED:
	default:
		tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbsetlbool() which = %d\n", which);
		return FAIL;
		break;
	}
}

/**
 * \ingroup dblib_core
 * \brief Set TDS version for future connections
 *
 */
RETCODE 
dbsetlversion (LOGINREC * login, BYTE version)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetlversion(%p, %x)\n", login, version);

	if( login == NULL ) {
		dbperror(NULL, SYBEASNL, 0);
		return FAIL;
	}

	assert(login->tds_login != NULL);
		
	switch (version) {
	case DBVER42:
		login->tds_login->tds_version = 0x402;
		return SUCCEED;
	case DBVER60:
		login->tds_login->tds_version = 0x700;
		return SUCCEED;
	case DBVERSION_100:
		tds_set_version(login->tds_login, 5, 0);
		return SUCCEED;
	case DBVERSION_71:
		tds_set_version(login->tds_login, 7, 1);
		return SUCCEED;
	case DBVERSION_72:
		tds_set_version(login->tds_login, 7, 2);
		return SUCCEED;
	case DBVERSION_73:
		tds_set_version(login->tds_login, 7, 3);
		return SUCCEED;
	case DBVERSION_74:
		tds_set_version(login->tds_login, 7, 4);
		return SUCCEED;
	}
	
	return FAIL;
}

static void
dbstring_free(DBSTRING ** dbstrp)
{
	DBSTRING *curr, *next;
	/* tdsdump_log(TDS_DBG_FUNC, "dbstring_free(%p)\n", dbstrp); */

	if (!dbstrp)
		return;

	curr = *dbstrp;
	*dbstrp = NULL;
	for (; curr; ) {
		next = curr->strnext;
		free(curr->strtext);
		free(curr);
		curr = next;
	}
}

static RETCODE
dbstring_concat(DBSTRING ** dbstrp, const char *p)
{
	DBSTRING **strp = dbstrp;

	/* tdsdump_log(TDS_DBG_FUNC, "dbstring_concat(%p, %s)\n", *dbstrp, p); */

	while (*strp != NULL) {
		strp = &((*strp)->strnext);
	}
	if ((*strp = tds_new(DBSTRING, 1)) == NULL) {
		dbperror(NULL, SYBEMEM, errno);
		return FAIL;
	}
	(*strp)->strtotlen = (DBINT)strlen(p);
	if (((*strp)->strtext = tds_new(BYTE, (*strp)->strtotlen)) == NULL) {
		TDS_ZERO_FREE(*strp);
		dbperror(NULL, SYBEMEM, errno);
		return FAIL;
	}
	memcpy((*strp)->strtext, p, (*strp)->strtotlen);
	(*strp)->strnext = NULL;
	return SUCCEED;
}

static RETCODE
dbstring_assign(DBSTRING ** dbstrp, const char *p)
{
	/* tdsdump_log(TDS_DBG_FUNC, "dbstring_assign(%p, %s)\n", *dbstrp, p); */

	dbstring_free(dbstrp);
	return dbstring_concat(dbstrp, p);
}

static DBINT
dbstring_length(DBSTRING * dbstr)
{
	DBINT len = 0;
	DBSTRING *next;

	/* tdsdump_log(TDS_DBG_FUNC, "dbstring_length(%p)\n", dbstr); */

	for (next = dbstr; next != NULL; next = next->strnext) {
		len += next->strtotlen;
	}
	return len;
}

static int
dbstring_getchar(DBSTRING * dbstr, int i)
{

	/* tdsdump_log(TDS_DBG_FUNC, "dbstring_getchar(%p, %d)\n", dbstr, i); */

	if (dbstr == NULL) {
		return -1;
	}
	if (i < 0) {
		return -1;
	}
	if (i < dbstr->strtotlen) {
		return dbstr->strtext[i];
	}
	return dbstring_getchar(dbstr->strnext, i - dbstr->strtotlen);
}

static char *
dbstring_get(DBSTRING * dbstr)
{
	DBSTRING *next;
	int len;
	char *ret;
	char *cp;

	/* tdsdump_log(TDS_DBG_FUNC, "dbstring_get(%p)\n", dbstr); */

	if (dbstr == NULL) {
		return NULL;
	}
	len = dbstring_length(dbstr);
	if ((ret = tds_new(char, len + 1)) == NULL) {
		dbperror(NULL, SYBEMEM, errno);
		return NULL;
	}
	cp = ret;
	for (next = dbstr; next != NULL; next = next->strnext) {
		memcpy(cp, next->strtext, next->strtotlen);
		cp += next->strtotlen;
	}
	*cp = '\0';
	return ret;
}

static const char *const opttext[DBNUMOPTIONS] = {
	"parseonly",
	"estimate",
	"showplan",
	"noexec",
	"arithignore",
	"nocount",
	"arithabort",
	"textlimit",
	"browse",
	"offsets",
	"statistics",
	"errlvl",
	"confirm",
	"spid",
	"buffer",
	"noautofree",
	"rowcount",
	"textsize",
	"language",
	"dateformat",
	"prpad",
	"prcolsep",
	"prlinelen",
	"prlinesep",
	"lfconvert",
	"datefirst",
	"chained",
	"fipsflagger",
	"transaction isolation level",
	"auth",
	"identity_insert",
	"no_identity_column",
	"cnv_date2char_short",
	"client cursors",
	"set time",
	"quoted_identifier"
};

static DBOPTION *
init_dboptions(void)
{
	DBOPTION *dbopts;
	int i;

	if ((dbopts = tds_new0(DBOPTION, DBNUMOPTIONS)) == NULL) {
		dbperror(NULL, SYBEMEM, errno);
		return NULL;
	}
	for (i = 0; i < DBNUMOPTIONS; i++) {
		dbopts[i].text = opttext[i];
		dbopts[i].param = NULL;
		dbopts[i].factive = FALSE;
	}
	dbstring_assign(&(dbopts[DBPRPAD].param), " ");
	dbstring_assign(&(dbopts[DBPRCOLSEP].param), " ");
	dbstring_assign(&(dbopts[DBPRLINELEN].param), "80");
	dbstring_assign(&(dbopts[DBPRLINESEP].param), "\n");
	dbstring_assign(&(dbopts[DBCLIENTCURSORS].param), " ");
	dbstring_assign(&(dbopts[DBSETTIME].param), " ");
	return dbopts;
}

/** \internal
 * \ingroup dblib_internal
 * \brief Form a connection with the server.
 *   
 * Called by the \c dbopen() macro, normally.  If FreeTDS was configured with \c --enable-msdblib, this
 * function is called by (exported) \c dbopen() function.  \c tdsdbopen is so-named to avoid
 * namespace conflicts with other database libraries that use the same function name.  
 * \param login \c LOGINREC* carrying the account information.
 * \param server name of the dataserver to connect to.  
 * \return valid pointer on successful login.  
 * \retval NULL insufficient memory, unable to connect for any reason.
 * \sa dbopen()
 * \todo use \c asprintf() to avoid buffer overflow.
 * \todo separate error messages for \em no-such-server and \em no-such-user. 
 */
DBPROCESS *
tdsdbopen(LOGINREC * login, const char *server, int msdblib)
{
	DBPROCESS *dbproc = NULL;
	TDSLOGIN *connection;

	char *tdsdump = getenv("TDSDUMP");
	if (tdsdump && *tdsdump) {
		tdsdump_open(tdsdump);
		tdsdump_log(TDS_DBG_FUNC, "tdsdbopen(%p, %s, [%s])\n", login, server? server : "0x0", msdblib? "microsoft" : "sybase");
	}

	/*
	 * Sybase supports the DSQUERY environment variable and falls back to "SYBASE" if server is NULL. 
	 * Microsoft uses a NULL or "" server to indicate a local server.  
	 * FIXME: support local server for win32.  
	 */
	if (!server && !msdblib) {
		if ((server = getenv("TDSQUERY")) == NULL)
			if ((server = getenv("DSQUERY")) == NULL)
				server = "SYBASE";
		tdsdump_log(TDS_DBG_FUNC, "tdsdbopen: servername set to %s\n", server);
	}

	if ((dbproc = tds_new0(DBPROCESS, 1)) == NULL) {
		dbperror(NULL, SYBEMEM, errno);
		return NULL;
	}
	dbproc->msdblib = msdblib;

	dbproc->dbopts = init_dboptions();
	if (dbproc->dbopts == NULL) {
		free(dbproc);
		return NULL;
	}
	tdsdump_log(TDS_DBG_FUNC, "tdsdbopen: dbproc->dbopts = %p\n", dbproc->dbopts);
	
	dbproc->dboptcmd = NULL;
	dbproc->avail_flag = TRUE;
	dbproc->command_state = DBCMDNONE;

	if (!tds_set_server(login->tds_login, server)) {
		dbperror(NULL, SYBEMEM, 0);
		free(dbproc);
		return NULL;
	}
	tdsdump_log(TDS_DBG_FUNC, "tdsdbopen: tds_set_server(%p, \"%s\")\n", login->tds_login, server);

	if ((dbproc->tds_socket = tds_alloc_socket(dblib_get_tds_ctx(), 512)) == NULL ){
		dbperror(NULL, SYBEMEM, 0);
		free(dbproc);
		return NULL;
	}
	

	tds_set_parent(dbproc->tds_socket, dbproc);

	dbproc->tds_socket->env_chg_func = db_env_chg;
	dbproc->envchange_rcv = 0;

	dbproc->dbcurdb[0] = '\0';
	dbproc->servcharset[0] = '\0';

	tdsdump_log(TDS_DBG_FUNC, "tdsdbopen: About to call tds_read_config_info...\n");

	connection = tds_read_config_info(dbproc->tds_socket, login->tds_login, g_dblib_ctx.tds_ctx->locale);
	if (!connection) {
		dbclose(dbproc);
		return NULL;
	}
	connection->option_flag2 &= ~TDS_ODBC_ON;	/* we're not an ODBC driver */
	tds_fix_login(connection);		/* initialize from Environment variables */

	dbproc->chkintr = NULL;
	dbproc->hndlintr = NULL;

	tds_mutex_lock(&dblib_mutex);

	/* override connection timeout if dbsetlogintime() was called */
	if (g_dblib_ctx.login_timeout > 0) {
		connection->connect_timeout = g_dblib_ctx.login_timeout;
	}

	/* override query timeout if dbsettime() was called */
	if (g_dblib_ctx.query_timeout > 0) {
		connection->query_timeout = g_dblib_ctx.query_timeout;
	}

	tds_mutex_unlock(&dblib_mutex);

	tdsdump_log(TDS_DBG_FUNC, "tdsdbopen: Calling tds_connect_and_login(%p, %p)\n",
		dbproc->tds_socket, connection);

	if (TDS_FAILED(tds_connect_and_login(dbproc->tds_socket, connection))) {
		tdsdump_log(TDS_DBG_ERROR, "tdsdbopen: tds_connect_and_login failed for \"%s\"!\n",
			tds_dstr_cstr(&connection->server_name));
		tds_free_login(connection);
		dbclose(dbproc);
		return NULL;
	}
	tds_free_login(connection);

	dbproc->dbbuf = NULL;
	dbproc->dbbufsz = 0;

	tds_mutex_lock(&dblib_mutex);
	dblib_add_connection(&g_dblib_ctx, dbproc->tds_socket);
	tds_mutex_unlock(&dblib_mutex);

	/* set the DBBUFFER capacity to nil */
	buffer_set_capacity(dbproc, 0);

	tds_mutex_lock(&dblib_mutex);

	if (g_dblib_ctx.recftos_filename != NULL) {
		char *temp_filename = NULL;
		const int len = asprintf(&temp_filename, "%s.%d", 
					 g_dblib_ctx.recftos_filename, g_dblib_ctx.recftos_filenum);
		if (len >= 0) {
			dbproc->ftos = fopen(temp_filename, "w");
			free(temp_filename);
			if (dbproc->ftos != NULL) {
				char timestr[256];
				fprintf(dbproc->ftos, "/* dbopen() at %s */\n", _dbprdate(timestr));
				fflush(dbproc->ftos);
				g_dblib_ctx.recftos_filenum++;
			}
		}
	}
	
	memcpy(dbproc->nullreps, default_null_representations, sizeof(default_null_representations));

	tds_mutex_unlock(&dblib_mutex);

	tdsdump_log(TDS_DBG_FUNC, "tdsdbopen: Returning dbproc = %p\n", dbproc);

	return dbproc;
}

/**
 * \ingroup dblib_core
 * \brief \c printf-like way to form SQL to send to the server.  
 *
 * Forms a command string and writes to the command buffer with dbcmd().  
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param fmt <tt> man vasprintf</tt> for details.  
 * \retval SUCCEED success.
 * \retval FAIL insufficient memory, or dbcmd() failed.
 * \sa dbcmd(), dbfreebuf(), dbgetchar(), dbopen(), dbstrcpy(), dbstrlen().
 */
RETCODE
dbfcmd(DBPROCESS * dbproc, const char *fmt, ...)
{
	va_list ap;
	char *s;
	int len;
	RETCODE ret;

	tdsdump_log(TDS_DBG_FUNC, "dbfcmd(%p, %s, ...)\n", dbproc, fmt);
	CHECK_CONN(FAIL);
	CHECK_NULP(fmt, "dbfcmd", 2, FAIL);
	
	va_start(ap, fmt);
	len = vasprintf(&s, fmt, ap);
	va_end(ap);

	if (len < 0) {
		dbperror(dbproc, SYBEMEM, errno);
		return FAIL;
	}

	ret = dbcmd(dbproc, s);
	free(s);

	return ret;
}

/**
 * \ingroup dblib_core
 * \brief \c Append SQL to the command buffer.  
 *
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param cmdstring SQL to append to the command buffer.  
 * \retval SUCCEED success.
 * \retval FAIL insufficient memory.  
 * \remarks set command state to \c  DBCMDPEND unless the command state is DBCMDSENT, in which case 
 * it frees the command buffer.  This latter may or may not be the Right Thing to do.  
 * \sa dbfcmd(), dbfreebuf(), dbgetchar(), dbopen(), dbstrcpy(), dbstrlen().
 */
RETCODE
dbcmd(DBPROCESS * dbproc, const char cmdstring[])
{
	size_t cmd_len, buf_len, newsz;

	tdsdump_log(TDS_DBG_FUNC, "dbcmd(%p, %s)\n", dbproc, cmdstring);
	CHECK_CONN(FAIL);
	CHECK_NULP(cmdstring, "dbcmd", 2, FAIL);

	dbproc->avail_flag = FALSE;

	tdsdump_log(TDS_DBG_FUNC, "dbcmd() bufsz = %d\n", dbproc->dbbufsz);

	if (dbproc->command_state == DBCMDSENT) {
		if (!dbproc->noautofree) {
			dbfreebuf(dbproc);
		}
	}

	buf_len = (dbproc->dbbufsz == 0) ? 0 : dbproc->dbbufsz - 1;
	cmd_len = strlen(cmdstring);
	newsz = buf_len + cmd_len + 1;
	if (newsz > 0x7fffffffu || !TDS_RESIZE(dbproc->dbbuf, newsz)) {
		dbperror(dbproc, SYBEMEM, errno);
		return FAIL;
	}
	memcpy(dbproc->dbbuf + buf_len, cmdstring, cmd_len);
	dbproc->dbbuf[newsz - 1] = 0;
	dbproc->dbbufsz = (int) newsz;

	dbproc->command_state = DBCMDPEND;

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief send the SQL command to the server and wait for an answer.  
 * 
 * Please be patient.  This function waits for the server to respond.   \c dbsqlexec is equivalent
 * to dbsqlsend() followed by dbsqlok(). 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval SUCCEED query was processed without errors.
 * \retval FAIL was returned by dbsqlsend() or dbsqlok().
 * \sa dbcmd(), dbfcmd(), dbnextrow(), dbresults(), dbretstatus(), dbsettime(), dbsqlok(), dbsqlsend()
 */
RETCODE
dbsqlexec(DBPROCESS * dbproc)
{
	RETCODE rc = FAIL;

	tdsdump_log(TDS_DBG_FUNC, "dbsqlexec(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	if (SUCCEED == (rc = dbsqlsend(dbproc))) {
		rc = dbsqlok(dbproc);
	}
	return rc;
}

/**
 * \ingroup dblib_core
 * \brief Change current database. 
 * 
 * Analagous to the unix command \c cd, dbuse() makes \a name the default database.  Waits for an answer
 * from the server.  
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param name database to use.
 * \retval SUCCEED query was processed without errors.
 * \retval FAIL query was not processed
 * \sa dbchange(), dbname().
 */
RETCODE
dbuse(DBPROCESS * dbproc, const char *name)
{
	RETCODE rc;
	char *query;

	tdsdump_log(TDS_DBG_FUNC, "dbuse(%p, %s)\n", dbproc, name);
	CHECK_CONN(FAIL);
	CHECK_NULP(name, "dbuse", 2, FAIL);

	if (!dbproc->tds_socket)
		return FAIL;

	/* quote name */
	query = tds_new(char, tds_quote_id(dbproc->tds_socket, NULL, name, -1) + 6);
	if (!query) {
		dbperror(dbproc, SYBEMEM, errno);
		return FAIL;
	}
	strcpy(query, "use ");
	/* TODO PHP suggest to quote by yourself with []... what should I do ?? quote or not ?? */
	if (name[0] == '[' && name[strlen(name)-1] == ']')
		strcat(query, name);
	else
		tds_quote_id(dbproc->tds_socket, query + 4, name, -1);

	rc = SUCCEED;
	if ((dbcmd(dbproc, query) == FAIL)
	    || (dbsqlexec(dbproc) == FAIL)
	    || (dbresults(dbproc) == FAIL)
	    || (dbcanquery(dbproc) == FAIL))
		rc = FAIL;
	free(query);
	return rc;
}

/**
 * \ingroup dblib_core
 * \brief Close a connection to the server and free associated resources.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa dbexit(), dbopen().
 */
void
dbclose(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;
	int i;
	char timestr[256];

	tdsdump_log(TDS_DBG_FUNC, "dbclose(%p)\n", dbproc);	
	CHECK_PARAMETER(dbproc, SYBENULL, );

	tds = dbproc->tds_socket;
	if (tds) {
		/* 
		 * this MUST be done before socket destruction
		 * it is possible that a TDSSOCKET is allocated on same position
		 */
		tds_mutex_lock(&dblib_mutex);
		dblib_del_connection(&g_dblib_ctx, dbproc->tds_socket);
		tds_mutex_unlock(&dblib_mutex);

		tds_close_socket(tds);
		tds_free_socket(tds);
		dblib_release_tds_ctx(1);
	}
	buffer_free(&(dbproc->row_buf));

	if (dbproc->ftos != NULL) {
		fprintf(dbproc->ftos, "/* dbclose() at %s */\n", _dbprdate(timestr));
		fclose(dbproc->ftos);
	}

	tds_free_bcpinfo(dbproc->bcpinfo);
	if (dbproc->hostfileinfo) {
		free(dbproc->hostfileinfo->hostfile);
		free(dbproc->hostfileinfo->errorfile);
		if (dbproc->hostfileinfo->host_columns) {
			for (i = 0; i < dbproc->hostfileinfo->host_colcount; i++) {
				free(dbproc->hostfileinfo->host_columns[i]->terminator);
				free(dbproc->hostfileinfo->host_columns[i]);
			}
			free(dbproc->hostfileinfo->host_columns);
		}
	}

	for (i = 0; i < DBNUMOPTIONS; i++) {
		dbstring_free(&(dbproc->dbopts[i].param));
	}
	free(dbproc->dbopts);

	dbstring_free(&(dbproc->dboptcmd));

	for (i=0; i < MAXBINDTYPES; i++) {
		if (dbproc->nullreps[i].bindval != default_null_representations[i].bindval)
			free((BYTE*)dbproc->nullreps[i].bindval);
	}

	dbfreebuf(dbproc);
	free(dbproc);
}

/**
 * \ingroup dblib_core
 * \brief Close server connections and free all related structures.  
 * 
 * \sa dbclose(), dbinit(), dbopen().
 * \todo breaks if ctlib/dblib used in same process.
 */
void
dbexit()
{
	TDSSOCKET *tds;
	DBPROCESS *dbproc;
	int i, list_size, count = 1;

	tdsdump_log(TDS_DBG_FUNC, "dbexit(void)\n");

	tds_mutex_lock(&dblib_mutex);

	if (--g_dblib_ctx.ref_count != 0) {
		tds_mutex_unlock(&dblib_mutex);
		return;
	}

	list_size = g_dblib_ctx.connection_list_size;

	for (i = 0; i < list_size; i++) {
		tds = g_dblib_ctx.connection_list[i];
		g_dblib_ctx.connection_list[i] = NULL;
		if (tds) {
			++count;
			dbproc = (DBPROCESS *) tds_get_parent(tds);
			tds_close_socket(tds);
			tds_free_socket(tds);
			if (dbproc) {
				/* avoid locking in dbclose */
				dbproc->tds_socket = NULL;
				dbclose(dbproc);
			}
		}
	}
	if (g_dblib_ctx.connection_list) {
		TDS_ZERO_FREE(g_dblib_ctx.connection_list);
		g_dblib_ctx.connection_list_size = 0;
	}

	tds_mutex_unlock(&dblib_mutex);

	dblib_release_tds_ctx(count);
}

typedef char prbuf_t[24];

static const char *
prdbresults_state(int retcode, prbuf_t buf)
{
	switch(retcode) {
	case _DB_RES_INIT:		return "_DB_RES_INIT";
	case _DB_RES_RESULTSET_EMPTY:	return "_DB_RES_RESULTSET_EMPTY";
	case _DB_RES_RESULTSET_ROWS:	return "_DB_RES_RESULTSET_ROWS";
	case _DB_RES_NEXT_RESULT:	return "_DB_RES_NEXT_RESULT";
	case _DB_RES_NO_MORE_RESULTS:	return "_DB_RES_NO_MORE_RESULTS";
	case _DB_RES_SUCCEED:		return "_DB_RES_SUCCEED";
	default:
		sprintf(buf, "oops: %u ??", retcode);
	}
	return buf;
}

static const char *
prdbretcode(RETCODE retcode, prbuf_t buf)
{
	switch(retcode) {
	case REG_ROW:		return "REG_ROW/MORE_ROWS";
	case NO_MORE_ROWS:	return "NO_MORE_ROWS";
	case BUF_FULL:		return "BUF_FULL";
	case NO_MORE_RESULTS:	return "NO_MORE_RESULTS";
	case SUCCEED:		return "SUCCEED";
	case FAIL:		return "FAIL";
	default:
		sprintf(buf, "oops: %u ??", retcode);
	}
	return buf;
}

static const char *
prretcode(int retcode, prbuf_t buf)
{
	switch(retcode) {
	case TDS_SUCCESS:		return "TDS_SUCCESS";
	case TDS_FAIL:			return "TDS_FAIL";
	case TDS_NO_MORE_RESULTS:	return "TDS_NO_MORE_RESULTS";
	case TDS_CANCELLED:		return "TDS_CANCELLED";
	default:
		sprintf(buf, "oops: %u ??", retcode);
	}
	return buf;
}

static const char *
prresult_type(int result_type, prbuf_t buf)
{
	switch(result_type) {
	case TDS_ROW_RESULT:		return "TDS_ROW_RESULT";
	case TDS_PARAM_RESULT:		return "TDS_PARAM_RESULT";
	case TDS_STATUS_RESULT:		return "TDS_STATUS_RESULT";
	case TDS_MSG_RESULT:		return "TDS_MSG_RESULT";
	case TDS_COMPUTE_RESULT:	return "TDS_COMPUTE_RESULT";
	case TDS_CMD_DONE:		return "TDS_CMD_DONE";
	case TDS_CMD_SUCCEED:		return "TDS_CMD_SUCCEED";
	case TDS_CMD_FAIL:		return "TDS_CMD_FAIL";
	case TDS_ROWFMT_RESULT:		return "TDS_ROWFMT_RESULT";
	case TDS_COMPUTEFMT_RESULT:	return "TDS_COMPUTEFMT_RESULT";
	case TDS_DESCRIBE_RESULT:	return "TDS_DESCRIBE_RESULT";
	case TDS_DONE_RESULT:		return "TDS_DONE_RESULT";
	case TDS_DONEPROC_RESULT:	return "TDS_DONEPROC_RESULT";
	case TDS_DONEINPROC_RESULT:	return "TDS_DONEINPROC_RESULT";
	case TDS_OTHERS_RESULT:		return "TDS_OTHERS_RESULT";
	default:
		sprintf(buf, "oops: %u ??", result_type);
	}
	return buf;
}

/**
 * \ingroup dblib_core
 * \brief Set up query results.  
 *
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval SUCCEED Some results are available.
 * \retval FAIL query was not processed successfully by the server
 * \retval NO_MORE_RESULTS query produced no results. 
 *
 * \remarks Call dbresults() after calling dbsqlexec() or dbsqlok(), or dbrpcsend() returns SUCCEED.  Unless
 *	one of them fails, dbresults will return either SUCCEED or NO_MORE_RESULTS.  
 *
 *	The meaning of \em results is very specific and not very intuitive.  Results are created by either
 *	- a SELECT statement
 * 	- a stored procedure
 *
 * 	When dbresults returns SUCCEED, therefore, it indicates the server processed the query successfully and 
 * 	that one or more of these is present:
 *	- metadata -- dbnumcols() returns 1 or more
 *	- data -- dbnextrow() returns SUCCEED
 *	- return status -- dbhasretstat() returns TRUE
 *	- output parameters -- dbnumrets() returns 1 or more
 *
 *	If none of the above are present, dbresults() returns NO_MORE_RESULTS.  
 * 	
 * 	SUCCEED does not imply that DBROWS() will return TRUE or even that dbnumcols() will return nonzero.  
 *	A general algorithm for reading results will call dbresults() until it return NO_MORE_RESULTS (or FAIL).  
 * 	An application should check for all the above kinds of results within the dbresults() loop.  
 * 
 * \sa dbsqlexec(), dbsqlok(), dbrpcsend(), dbcancel(), DBROWS(), dbnextrow(), dbnumcols(), dbhasretstat(), dbretstatus(), dbnumrets()
 */
RETCODE
dbresults(DBPROCESS * dbproc)
{
	RETCODE erc = _dbresults(dbproc);
	prbuf_t buf;

	tdsdump_log(TDS_DBG_FUNC, "dbresults returning %d (%s)\n", erc, prdbretcode(erc, buf));
	return erc;
}

static RETCODE
_dbresults(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;
	int result_type = 0, done_flags;
	prbuf_t prbuf1, prbuf2;

	tdsdump_log(TDS_DBG_FUNC, "dbresults(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	tds = dbproc->tds_socket;

	tdsdump_log(TDS_DBG_FUNC, "dbresults: dbresults_state is %d (%s)\n", 
					dbproc->dbresults_state, prdbresults_state(dbproc->dbresults_state, prbuf1));
	switch ( dbproc->dbresults_state ) {
	case _DB_RES_SUCCEED:
		dbproc->dbresults_state = _DB_RES_NEXT_RESULT;
		return SUCCEED;
		break;
	case _DB_RES_RESULTSET_ROWS:
		dbperror(dbproc, SYBERPND, 0); /* dbresults called while rows outstanding.... */
		return FAIL;
		break;
	case _DB_RES_NO_MORE_RESULTS:
		return NO_MORE_RESULTS;
		break;
	default:
		break;
	}

	for (;;) {
		TDSRET retcode = tds_process_tokens(tds, &result_type, &done_flags, TDS_TOKEN_RESULTS);

		tdsdump_log(TDS_DBG_FUNC, "dbresults() tds_process_tokens returned %d (%s),\n\t\t\tresult_type %s\n", 
						retcode, prretcode(retcode, prbuf1), prresult_type(result_type, prbuf2));

		switch (retcode) {

		case TDS_SUCCESS:

			switch (result_type) {
	
			case TDS_ROWFMT_RESULT:
				buffer_free(&dbproc->row_buf);
				buffer_alloc(dbproc);
				dbproc->dbresults_state = _DB_RES_RESULTSET_EMPTY;
				break;
	
			case TDS_COMPUTEFMT_RESULT:
				break;
	
			case TDS_ROW_RESULT:
			case TDS_COMPUTE_RESULT:
	
				dbproc->dbresults_state = _DB_RES_RESULTSET_ROWS;
				return SUCCEED;
				break;
	
			case TDS_DONE_RESULT:
			case TDS_DONEPROC_RESULT:
				tdsdump_log(TDS_DBG_FUNC, "dbresults(): dbresults_state is %d (%s)\n", 
						dbproc->dbresults_state, prdbresults_state(dbproc->dbresults_state, prbuf1));

				/* A done token signifies the end of a logical command.
				 * There are three possibilities:
				 * 1. Simple command with no result set, i.e. update, delete, insert
				 * 2. Command with result set but no rows
				 * 3. Command with result set and rows
				 */
				switch (dbproc->dbresults_state) {

				case _DB_RES_INIT:
				case _DB_RES_NEXT_RESULT:
					dbproc->dbresults_state = _DB_RES_NEXT_RESULT;
					if (done_flags & TDS_DONE_ERROR)
						return FAIL;
					if (result_type == TDS_DONE_RESULT)
						return SUCCEED;
					break;

				case _DB_RES_RESULTSET_EMPTY:
				case _DB_RES_RESULTSET_ROWS:
					dbproc->dbresults_state = _DB_RES_NEXT_RESULT;
					return SUCCEED;
					break;
				default:
					assert(0);
					break;
				}
				break;

			case TDS_DONEINPROC_RESULT:
				/* 
				 * Return SUCCEED on a command within a stored procedure
				 * only if the command returned a result set. 
				 */
				switch (dbproc->dbresults_state) {
				case _DB_RES_INIT:  
				case _DB_RES_NEXT_RESULT: 
					dbproc->dbresults_state = _DB_RES_NEXT_RESULT;
					break;
				case _DB_RES_RESULTSET_EMPTY :
				case _DB_RES_RESULTSET_ROWS : 
					dbproc->dbresults_state = _DB_RES_NEXT_RESULT;
					return SUCCEED;
					break;
				case _DB_RES_NO_MORE_RESULTS:
				case _DB_RES_SUCCEED:
					break;
				}
				break;

			case TDS_STATUS_RESULT:
			case TDS_MSG_RESULT:
			case TDS_DESCRIBE_RESULT:
			case TDS_PARAM_RESULT:
			default:
				break;
			}

			break;

		case TDS_NO_MORE_RESULTS:
			dbproc->dbresults_state = _DB_RES_NO_MORE_RESULTS;
			return NO_MORE_RESULTS;
			break;

		default:
			assert(TDS_FAILED(retcode));
			dbproc->dbresults_state = _DB_RES_INIT;
			return FAIL;
			break;
		}
	}
}


/**
 * \ingroup dblib_core
 * \brief Return number of regular columns in a result set.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa dbcollen(), dbcolname(), dbnumalts().
 */
int
dbnumcols(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbnumcols(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);

	if (dbproc && dbproc->tds_socket && dbproc->tds_socket->res_info)
		return dbproc->tds_socket->res_info->num_cols;
	return 0;
}

/**
 * \ingroup dblib_core
 * \brief Return name of a regular result column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting with 1.  
 * \return pointer to ASCII null-terminated string, the name of the column. 
 * \retval NULL \a column is not in range.
 * \sa dbcollen(), dbcoltype(), dbdata(), dbdatlen(), dbnumcols().
 * \bug Relies on ASCII column names, post iconv conversion.  
 *      Will not work as described for UTF-8 or UCS-2 clients.  
 *      But maybe it shouldn't.  
 */
char *
dbcolname(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;
	
	tdsdump_log(TDS_DBG_FUNC, "dbcolname(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return NULL;
		
	return tds_dstr_buf(&colinfo->column_name);
}

/**
 * \ingroup dblib_core
 * \brief Read a row from the row buffer.
 * 
 * When row buffering is enabled (DBBUFFER option is on), the client can use dbgetrow() to re-read a row previously fetched 
 * with dbnextrow().  The effect is to move the row pointer -- analogous to fseek() -- back to \a row.  
 * Calls to dbnextrow() read from \a row + 1 until the buffer is exhausted, at which point it resumes
 * its normal behavior, except that as each row is fetched from the server, it is added to the row
 * buffer (in addition to being returned to the client).  When the buffer is filled, dbnextrow()  returns 
 * \c FAIL until the buffer is at least partially emptied with dbclrbuf().
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param row Nth row to read, starting with 1.
 * \retval REG_ROW returned row is a regular row.
 * \returns computeid when returned row is a compute row.
 * \retval NO_MORE_ROWS no such row in the row buffer.  Current row is unchanged.
 * \retval FAIL unsuccessful; row buffer may be full.  
 * \sa dbaltbind(), dbbind(), dbclrbuf(), DBCURROW(), DBFIRSTROW(), DBLASTROW(), dbnextrow(), dbsetrow().
 */
RETCODE
dbgetrow(DBPROCESS * dbproc, DBINT row)
{
	RETCODE result = FAIL;
	const int idx = buffer_row2idx(&dbproc->row_buf, row);

	tdsdump_log(TDS_DBG_FUNC, "dbgetrow(%p, %d)\n", dbproc, row);
	CHECK_CONN(FAIL);

	if (-1 == idx)
		return NO_MORE_ROWS;

	dbproc->row_buf.current = idx;
	buffer_transfer_bound_data(&dbproc->row_buf, TDS_ROW_RESULT, 0, dbproc, idx);
	result = REG_ROW;

	return result;
}

/**
 * \ingroup dblib_core
 * \brief Define substitution values to be used when binding null values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param bindtype	type of binding to which the substitute value will apply. 
 * \param bindlen 	size of the substitute value you are supplying, in bytes. 
 * 			Ignored except for CHARBIND and BINARYBIND. 
 * \param bindval 	pointer to a buffer containing the substitute value.
 * \retval SUCCEED query was processed without errors.
 * \retval FAIL query was not processed
 * \sa dbaltbind(), dbbind(), dbconvert(), dbnullbind().
 */
RETCODE
dbsetnull(DBPROCESS * dbproc, int bindtype, int bindlen, BYTE *bindval)
{
	BYTE *pval;
	
	tdsdump_log(TDS_DBG_FUNC, "dbsetnull(%p, %d, %d, %p)\n", dbproc, bindtype, bindlen, bindval);
	
	CHECK_CONN(FAIL);
	CHECK_PARAMETER(bindval, SYBENBVP, FAIL);
	
	switch (bindtype) {
	case DATETIMEBIND:
	case DECIMALBIND:
	case SRCDECIMALBIND:
	case FLT8BIND:
	case INTBIND:
	case MONEYBIND:
	case NUMERICBIND:
	case SRCNUMERICBIND:
	case REALBIND:
	case SMALLBIND:
	case SMALLDATETIMEBIND:
	case SMALLMONEYBIND:
	case TINYBIND:
	case BIGINTBIND:
	case DATEBIND:
	case TIMEBIND:
	case BIGDATETIMEBIND:
	case BIGTIMEBIND:
		bindlen = (int)default_null_representations[bindtype].len;
		break;

	case CHARBIND:
	case BINARYBIND:
		CHECK_PARAMETER(bindlen >= 0, SYBEBBL, FAIL);
		break;
		
	case NTBSTRINGBIND:	bindlen = (int)strlen((char *) bindval);
		break;
	case STRINGBIND:	bindlen = (int)strlen((char *) bindval);
		break;
	case VARYBINBIND:	bindlen = ((DBVARYBIN*) bindval)->len;
		break;
	case VARYCHARBIND:	bindlen = ((DBVARYCHAR*) bindval)->len;
		break;

#if 0
	case SENSITIVITYBIND:
	case BOUNDARYBIND:
#endif
	default:
		dbperror(dbproc, SYBEBTYP, 0);
		return FAIL;
	}

	if ((pval = tds_new(BYTE, bindlen)) == NULL) {
		dbperror(dbproc, SYBEMEM, errno);
		return FAIL;
	}
	
	/* free any prior allocation */
	if (dbproc->nullreps[bindtype].bindval != default_null_representations[bindtype].bindval)
		free((BYTE*)dbproc->nullreps[bindtype].bindval);

	memcpy(pval, bindval, bindlen);
	
	dbproc->nullreps[bindtype].bindval = pval;
	dbproc->nullreps[bindtype].len = bindlen;

	tdsdump_dump_buf(TDS_DBG_NETWORK, "null representation set ", pval,  bindlen);
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Make a buffered row "current" without fetching it into bound variables.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval MORE_ROWS row found
 * \retval NO_MORE_ROWS row not found
 * \retval FAIL \a dbproc is dead or not enabled
 * \sa dbaltbind(), dbbind(), dbcanquery(), dbclrbuf(), dbgetrow(), dbnextrow(), dbprrow().
 */
STATUS
dbsetrow(DBPROCESS * dbproc, DBINT row)
{
	const int idx = buffer_row2idx(&dbproc->row_buf, row);

	tdsdump_log(TDS_DBG_FUNC, "dbsetrow(%p, %d)\n", dbproc, row);
	CHECK_CONN(FAIL);

	if (-1 == idx)
		return NO_MORE_ROWS;
		
	dbproc->row_buf.current = idx;
		
	/* FIXME: should determine REG_ROW or compute_id; */
	return REG_ROW;	
}

/**
 * \ingroup dblib_core
 * \brief Read result row into the row buffer and into any bound host variables.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval REG_ROW regular row has been read.
 * \returns computeid when a compute row is read. 
 * \retval BUF_FULL reading next row would cause the buffer to be exceeded (and buffering is turned on).
 * No row was read from the server
 * \sa dbaltbind(), dbbind(), dbcanquery(), dbclrbuf(), dbgetrow(), dbprrow(), dbsetrow().
 */
struct pivot_t;
STATUS
dbnextrow(DBPROCESS * dbproc)
{
	TDSRESULTINFO *resinfo;
	TDSSOCKET *tds;
	STATUS result = FAIL;
	TDS_INT res_type;
	TDS_INT computeid;
	int idx; /* row buffer index.  Unless DBUFFER is on, idx will always be 0. */
	struct pivot_t *pivot;
	prbuf_t prbuf;

	tdsdump_log(TDS_DBG_FUNC, "dbnextrow(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	tds = dbproc->tds_socket;
	resinfo = tds->res_info;

	tdsdump_log(TDS_DBG_FUNC, "dbnextrow() dbresults_state = %d (%s)\n", 
					dbproc->dbresults_state, prdbresults_state(dbproc->dbresults_state, prbuf));

	if (!resinfo || dbproc->dbresults_state != _DB_RES_RESULTSET_ROWS) {
		/* no result set or result set empty (no rows) */
		tdsdump_log(TDS_DBG_FUNC, "leaving dbnextrow() returning %d (NO_MORE_ROWS)\n", NO_MORE_ROWS);
		return dbproc->row_type = NO_MORE_ROWS;
	}

	/*
	 * Try to get the dbproc->row_buf.current item from the buffered rows, if any.  
	 * Else read from the stream, unless the buffer is exhausted.  
	 * If no rows are read, DBROWTYPE() will report NO_MORE_ROWS. 
	 */
	dbproc->row_type = NO_MORE_ROWS; 
	computeid = REG_ROW;
	if (-1 != (idx = buffer_current_index(dbproc))) {			
		/*
		 * Cool, the item we want is already there
		 */
		result = dbproc->row_type = REG_ROW;
		res_type = TDS_ROW_RESULT;
		
	} else if (buffer_is_full(&dbproc->row_buf)) {
		
		result = BUF_FULL;
		res_type = TDS_ROWFMT_RESULT;
	
	} else if ((pivot = dbrows_pivoted(dbproc)) != NULL) {
	
		tdsdump_log(TDS_DBG_FUNC, "returning pivoted row\n");
		return dbnextrow_pivoted(dbproc, pivot);

	} else {
		const int mask = TDS_STOPAT_ROWFMT|TDS_RETURN_DONE|TDS_RETURN_ROW|TDS_RETURN_COMPUTE;
		TDS_INT8 row_count = TDS_NO_COUNT;
		bool rows_set = false;
		buffer_save_row(dbproc);

		/* Get the row from the TDS stream.  */
again:
		switch (tds_process_tokens(tds, &res_type, NULL, mask)) {
		case TDS_SUCCESS:
			if (res_type == TDS_ROW_RESULT || res_type == TDS_COMPUTE_RESULT) {
				if (res_type == TDS_COMPUTE_RESULT)
					computeid = tds->current_results->computeid;
				/* Add the row to the row buffer, whose capacity is always at least 1 */
				resinfo = tds->current_results;
				idx = buffer_add_row(dbproc, resinfo);
				assert(idx != -1);
				result = dbproc->row_type = (res_type == TDS_ROW_RESULT)? REG_ROW : computeid;
#if 0 /* TODO */
				tds_process_tokens(tds, &res_type, NULL, TDS_TOKEN_TRAILING);
#endif
				break;
			}
			/* allows to process trailing tokens */
			if (res_type == TDS_DONEINPROC_RESULT) {
				if (!rows_set)
					row_count = tds->rows_affected;
				rows_set = true;
				goto again;
			}
			/* fall through */
		case TDS_NO_MORE_RESULTS:
			dbproc->dbresults_state = _DB_RES_NEXT_RESULT;
			result = NO_MORE_ROWS;
			break;
		default:
			tdsdump_log(TDS_DBG_FUNC, "unexpected: leaving dbnextrow() returning FAIL\n");
			return FAIL;
			break;
		}
		if (rows_set)
			tds->rows_affected = row_count;
	}

	if (res_type == TDS_ROW_RESULT || res_type == TDS_COMPUTE_RESULT) {
		/*
		 * Transfer the data from the row buffer to the bound variables.  
		 */
		buffer_transfer_bound_data(&dbproc->row_buf, res_type, computeid, dbproc, idx);
	}
	
	if (res_type == TDS_COMPUTE_RESULT) {
		tdsdump_log(TDS_DBG_FUNC, "leaving dbnextrow() returning compute_id %d\n", result);
	} else {
		tdsdump_log(TDS_DBG_FUNC, "leaving dbnextrow() returning %s\n", prdbretcode(result, prbuf));
	}
	return result;
} /* dbnextrow()  */

static TDS_SERVER_TYPE
dblib_bound_type(int bindtype)
{
	switch (bindtype) {
	case CHARBIND:
	case STRINGBIND:
	case NTBSTRINGBIND:
		return SYBCHAR;
		break;
	case FLT8BIND:
		return SYBFLT8;
		break;
	case REALBIND:
		return SYBREAL;
		break;
	case INTBIND:
		return SYBINT4;
		break;
	case SMALLBIND:
		return SYBINT2;
		break;
	case TINYBIND:
		return SYBINT1;
		break;
	case BIGINTBIND:
		return SYBINT8;
		break;
	case DATETIMEBIND:
		return SYBDATETIME;
		break;
	case SMALLDATETIMEBIND:
		return SYBDATETIME4;
		break;
	case DATEBIND:
		return SYBDATE;
		break;
	case TIMEBIND:
		return SYBTIME;
		break;
	case BIGDATETIMEBIND:
		return SYB5BIGDATETIME;
		break;
	case BIGTIMEBIND:
		return SYB5BIGTIME;
		break;
	case MONEYBIND:
		return SYBMONEY;
		break;
	case SMALLMONEYBIND:
		return SYBMONEY4;
		break;
	case BINARYBIND:
		return SYBBINARY;
		break;
	case VARYBINBIND:
		return SYBVARBINARY;
		break;
	case VARYCHARBIND:
		return SYBVARCHAR;
		break;
	case BITBIND:
		return SYBBIT;
		break;
	case NUMERICBIND:
	case SRCNUMERICBIND:
	case DECIMALBIND:
	case SRCDECIMALBIND:
		return SYBNUMERIC;
		break;
	case DATETIME2BIND:
		return SYBMSDATETIMEOFFSET;
		break;
	default:
		return TDS_INVALID_TYPE;
	}
}

/**
 * \ingroup dblib_core
 * \brief Convert one datatype to another.
 *
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param srctype datatype of the data to convert. 
 * \param src buffer to convert
 * \param srclen length of \a src
 * \param desttype target datatype
 * \param dest output buffer
 * \param destlen size of \a dest
 * \returns	On success, the count of output bytes in \a dest, else -1. On failure, it will call any user-supplied error handler. 
 * \remarks 	 Causes of failure: 
 * 		- No such conversion unavailable. 
 * 		- Character data output was truncated, or numerical data overflowed or lost precision. 
 * 		- In converting character data to one of the numeric types, the string could not be interpreted as a number.  
 *		
 * Conversion functions are handled in the TDS layer.
 * 
 * The main reason for this is that \c ct-lib and \c ODBC (and presumably \c DBI) need
 * to be able to do conversions between datatypes. This is possible because
 * the format of complex data (dates, money, numeric, decimal) is defined by
 * its representation on the wire; thus what we call \c DBMONEY is exactly its
 * format on the wire. CLIs that need a different representation (ODBC?) 
 * need to convert from this format anyway, so the code would already be in
 * place.
 * 
 * Each datatype is also defined by its Server-type so all CLIs should be 
 * able to map native types to server types as well.
 *
 * tds_convert() copies from src to dest and returns the output data length,
 * period.  All padding and termination is the responsibility of the API library
 * and is done post-conversion.  The peculiar rule in dbconvert() is that
 * a \a destlen of -1 and a \a desttype of \c SYBCHAR means the output buffer
 * should be null-terminated.
 *  
 * \sa dbaltbind(), dbaltbind_ps(), dbbind(), dbbind_ps(), dbconvert_ps(), dberrhandle(), dbsetnull(), dbsetversion(), dbwillconvert().
 * \todo What happens if client does not reset values? 
 * \todo Microsoft and Sybase define this function differently.  
 */
DBINT
dbconvert_ps(DBPROCESS * dbproc, int db_srctype, const BYTE * src, DBINT srclen,
	     int db_desttype, BYTE * dest, DBINT destlen, DBTYPEINFO * typeinfo)
{
	CONV_RESULT dres;
	DBINT ret;
	int i;
	int len;
	TDS_SERVER_TYPE srctype, desttype;

	tdsdump_log(TDS_DBG_FUNC, "dbconvert_ps(%p, %s, %p, %d, %s, %p, %d, %p)\n",
		    dbproc, tds_prdatatype(db_srctype), src, srclen,
		    tds_prdatatype(db_desttype), dest, destlen, typeinfo);
	/* dbproc and src can be NULLs */
	CHECK_PARAMETER(dest, SYBEACNV, -1);

	DBPERROR_RETURN(!is_tds_type_valid(db_srctype), SYBEUDTY);
	srctype = (TDS_SERVER_TYPE) db_srctype;
	DBPERROR_RETURN(!is_tds_type_valid(db_desttype), SYBEUDTY);
	desttype = (TDS_SERVER_TYPE) db_desttype;

	if (is_numeric_type(desttype)) {
		TDS_NUMERIC *d = &dres.n;

		if (typeinfo == NULL) {
			if (is_numeric_type(srctype)) {
				DBNUMERIC *s = (DBNUMERIC *) src;
				d->precision = s->precision;
				d->scale = s->scale;
			} else {
				d->precision = 18;
				d->scale = 0;
			}
		} else {
			d->precision = typeinfo->precision;
			d->scale = typeinfo->scale;
		}
	}

	if (0 == destlen) 
		return 0; 

	if (src == NULL || srclen == 0) {
		int bind = dbbindtype(desttype);
		int size = tds_get_size_by_type(desttype);

		if (bind == NTBSTRINGBIND) {
			if (destlen > 0) {
				size = destlen;
				bind = CHARBIND;
			} else { 
				size = 1;
				bind = NTBSTRINGBIND;
			}
		} else if (bind == BINARYBIND) {
			if (destlen > 0)
				size = destlen;
			else
				size = 0;
		}

		dbgetnull(dbproc, bind, size, dest);
		return size;
	}

	/* srclen of -1 means the source data is definitely NULL terminated */
	if (srclen == -1)
		srclen = (int)strlen((const char *) src);

	/* often times we are asked to convert a data type to itself */
	if (srctype == desttype && !is_numeric_type(desttype)) {
		ret = -2;  /* to make sure we always set it */
		tdsdump_log(TDS_DBG_INFO1, "dbconvert_ps() srctype == desttype\n");
		switch (desttype) {

		case SYBBINARY:
		case SYBVARBINARY:
		case SYBIMAGE:
			if (srclen > destlen && destlen >= 0) {
				dbperror(dbproc, SYBECOFL, 0);
				ret = -1;
			} else {
				memcpy(dest, src, srclen);
				if (srclen < destlen)
					memset(dest + srclen, 0, destlen - srclen);
				ret = srclen;
			}
			break;

		case SYBCHAR:
		case SYBVARCHAR:
		case SYBTEXT:
			/* srclen of -1 means the source data is definitely NULL terminated */
			if (srclen == -1)
				srclen = (int)strlen((const char *) src);

			switch (destlen) {
			case  0:	/* nothing to copy */
				ret = 0;
				break;
			case -1:	/* rtrim and null terminate */
				while (srclen && src[srclen - 1] == ' ') {
					--srclen;
				}
				/* fall thru */
			case -2:	/* just null terminate */
				memcpy(dest, src, srclen);
				dest[srclen] = '\0';
				ret = srclen;
				break;
			default:
				assert(destlen > 0);
				if (destlen < 0 || srclen > destlen) {
					dbperror(dbproc, SYBECOFL, 0);
					ret = -1;
				} else {
					memcpy(dest, src, srclen);
					for (i = srclen; i < destlen; i++)
						dest[i] = ' ';
					ret = srclen;
				}
				break;
			}
			break;
		case SYBINT1:
		case SYBINT2:
		case SYBINT4:
		case SYBINT8:
		case SYBFLT8:
		case SYBREAL:
		case SYBBIT:
		case SYBBITN:
		case SYBMONEY:
		case SYBMONEY4:
		case SYBDATETIME:
		case SYBDATETIME4:
		case SYBDATE:
		case SYBTIME:
		case SYB5BIGDATETIME:
		case SYB5BIGTIME:
		case SYBUNIQUE:
			ret = tds_get_size_by_type(desttype);
			memcpy(dest, src, ret);
			break;

		case SYBMSDATE:
		case SYBMSTIME:
		case SYBMSDATETIME2:
		case SYBMSDATETIMEOFFSET:
			ret = sizeof(TDS_DATETIMEALL);
			memcpy(dest, src, ret);
			break;

		default:
			ret = -1;
			break;
		}
		assert(ret > -2);
		return ret;
	}
	/* end srctype == desttype */

	/*
	 * Character types need no conversion.  Just move the data.
	 */
	if (is_similar_type(srctype, desttype)) {
		if (src && dest && srclen > 0 && destlen >= srclen) {
			memcpy(dest, src, srclen);
			return srclen;
		}
	}

	tdsdump_log(TDS_DBG_INFO1, "dbconvert_ps() calling tds_convert\n");

	len = tds_convert(g_dblib_ctx.tds_ctx, srctype, (const TDS_CHAR *) src, srclen, desttype, &dres);
	tdsdump_log(TDS_DBG_INFO1, "dbconvert_ps() called tds_convert returned %d\n", len);

	if (len < 0) {
		_dblib_convert_err(dbproc, len);
		return -1;
	}

	switch (desttype) {
	case SYBBINARY:
	case SYBVARBINARY:
	case SYBIMAGE:
		if (len > destlen && destlen >= 0) {
			dbperror(dbproc, SYBECOFL, 0);
			ret = -1;
		} else {
			memcpy(dest, dres.ib, len);
			if (len < destlen)
				memset(dest + len, 0, destlen - len);
			ret = len;
		}
		free(dres.ib);
		break;
	case SYBINT1:
	case SYBINT2:
	case SYBINT4:
	case SYBINT8:
	case SYBFLT8:
	case SYBREAL:
	case SYBBIT:
	case SYBBITN:
	case SYBMONEY:
	case SYBMONEY4:
	case SYBDATETIME:
	case SYBDATETIME4:
	case SYBTIME:
	case SYBDATE:
	case SYB5BIGDATETIME:
	case SYB5BIGTIME:
	case SYBUNIQUE:
	case SYBMSDATE:
	case SYBMSTIME:
	case SYBMSDATETIME2:
	case SYBMSDATETIMEOFFSET:
	case SYBNUMERIC:
	case SYBDECIMAL:
		memcpy(dest, &(dres.ti), len);
		ret = len;
		break;
	case SYBCHAR:
	case SYBVARCHAR:
	case SYBTEXT:
		tdsdump_log(TDS_DBG_INFO1, "dbconvert_ps() outputting %d bytes character data destlen = %d \n", len, destlen);

		if (destlen < -2)
			destlen = 0;	/* failure condition */

		switch (destlen) {
		case 0:
			ret = -1;
			break;
		case -1:	/* rtrim and null terminate */
			for (i = len - 1; i >= 0 && dres.c[i] == ' '; --i) {
				len = i;
			}
			memcpy(dest, dres.c, len);
			dest[len] = '\0';
			ret = len;
			break;
		case -2:	/* just null terminate */
			memcpy(dest, dres.c, len);
			dest[len] = 0;
			ret = len;
			break;
		default:
			assert(destlen > 0);
			if (destlen < 0 || len > destlen) {
				dbperror(dbproc, SYBECOFL, 0);
				ret = -1;
				tdsdump_log(TDS_DBG_INFO1, "%d bytes type %d -> %d, destlen %d < %d required\n",
					    srclen, srctype, desttype, destlen, len);
				break;
			}
			/* else pad with blanks */
			memcpy(dest, dres.c, len);
			for (i = len; i < destlen; i++)
				dest[i] = ' ';
			ret = len;

			break;
		}

		free(dres.c);

		break;
	default:
		tdsdump_log(TDS_DBG_INFO1, "error: dbconvert_ps(): unrecognized desttype %d \n", desttype);
		ret = -1;
		break;

	}
	return (ret);
}

/**
 * \ingroup dblib_core
 * \brief cf. dbconvert_ps(), above
 * 
 * \em Sybase: Convert numeric types.
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param srctype datatype of the data to convert. 
 * \param src buffer to convert
 * \param srclen length of \a src
 * \param desttype target datatype
 * \param dest output buffer
 * \param destlen size of \a dest
 * \param typeinfo address of a \c DBTYPEINFO structure that governs the precision & scale of the output, may be \c NULL.
 * \sa dbaltbind(), dbaltbind_ps(), dbbind(), dbbind_ps(), dbconvert_ps(), dberrhandle(), dbsetnull(), dbsetversion(), dbwillconvert().
 */
DBINT
dbconvert(DBPROCESS * dbproc,
	  int srctype, const BYTE * src, DBINT srclen, int desttype, BYTE * dest, DBINT destlen)
{
	DBTYPEINFO ti, *pti = NULL;

	tdsdump_log(TDS_DBG_FUNC, "dbconvert(%p)\n", dbproc);
	/* dbproc can be NULL*/

	DBPERROR_RETURN(!is_tds_type_valid(desttype), SYBEUDTY);

	if (is_numeric_type(desttype)) {
		DBNUMERIC *num;

		/* FIXME what happen if client do not reset values ??? */
		if (dbproc->msdblib) {
			num = (DBNUMERIC *) dest;
			ti.precision = num->precision;
			ti.scale = num->scale;
			pti = &ti;
		} else {
			/* for Sybase passing NULL as DBTYPEINFO is fine */
		}
	}

	return dbconvert_ps(dbproc, srctype, src, srclen, desttype, dest, destlen, pti);
}

/**
 * \ingroup dblib_core
 * \brief Tie a host variable to a resultset column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth column, starting at 1.
 * \param vartype datatype of the host variable that will receive the data
 * \param varlen size of host variable pointed to \a varaddr
 * \param varaddr address of host variable
 * \retval SUCCEED everything worked.
 * \retval FAIL no such \a column or no such conversion possible, or target buffer too small.
 * \sa 
 */
RETCODE
dbbind(DBPROCESS * dbproc, int column, int vartype, DBINT varlen, BYTE * varaddr)
{
	TDSCOLUMN *colinfo = NULL;
	TDSRESULTINFO* results;
	TDS_SERVER_TYPE srctype, desttype;

	tdsdump_log(TDS_DBG_FUNC, "dbbind(%p, %d, %d, %d, %p)\n", dbproc, column, vartype, varlen, varaddr);
	CHECK_CONN(FAIL);
	CHECK_PARAMETER(varaddr, SYBEABNV, FAIL);
	
	results = dbproc->tds_socket->res_info;
	
	if (results == NULL || results->num_cols < column || column < 1) {
		dbperror(dbproc, SYBEABNC, 0);
		return FAIL;
	}
	
	if (varlen < 0) {
		switch (vartype) {
		case CHARBIND:
		case STRINGBIND:
		case NTBSTRINGBIND:
		case VARYCHARBIND:
		case VARYBINBIND:
			/* 
			 * No message for this error.  Documentation doesn't define varlen < 0, but 
			 * experimentation with Sybase db-lib shows it's accepted as if zero. 
			 */
			tdsdump_log(TDS_DBG_FUNC, "dbbind: setting varlen (%d) to 0\n", varlen);
			varlen = 0;
			break;
		}
	}

	if (0 == varlen) {		/* "Note that if varlen is 0, no padding takes place." */
		switch (vartype) {
		case CHARBIND:
		case STRINGBIND:
		case NTBSTRINGBIND:
			varlen = -1;
			break;
		default:
			break;		/* dbconvert: "The destlen is ignored for all fixed-length, non-NULL data types." */
		}
	}

	dbproc->avail_flag = FALSE;

	colinfo = dbproc->tds_socket->res_info->columns[column - 1];
	srctype = tds_get_conversion_type(colinfo->column_type, colinfo->column_size);
	desttype = dblib_bound_type(vartype);
	if (desttype == TDS_INVALID_TYPE) {
		dbperror(dbproc, SYBEBTYP, 0);
		return FAIL;
	}

	if (!dbwillconvert(srctype, desttype)) {
		dbperror(dbproc, SYBEABMT, 0);
		return FAIL;
	}

	colinfo->column_varaddr = (char *) varaddr;
	colinfo->column_bindtype = vartype;
	colinfo->column_bindlen = varlen;

	return SUCCEED;
}				/* dbbind()  */

/**
 * \ingroup dblib_core
 * \brief set name and location of the \c interfaces file FreeTDS should use to look up a servername.
 * 
 * Does not affect lookups or location of \c freetds.conf.  
 * \param filename name of \c interfaces 
 * \sa dbopen()
 */
void
dbsetifile(char *filename)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetifile(%s)\n", filename? filename : "0x00");
	if (filename == NULL) { 
		dbperror(NULL, SYBENULP, 0); 
		return;
	}
	tds_set_interfaces_file_loc(filename);
}

/**
 * \ingroup dblib_core
 * \brief Tie a null-indicator to a regular result column.
 * 
 * 
 * When a row is fetched, the indicator variable tells the state of the column's data.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth column in the result set, starting with 1.
 * \param indicator address of host variable.
 * \retval SUCCEED variable accepted.
 * \retval FAIL \a indicator is NULL or \a column is out of range. 
 * \remarks Contents of \a indicator are set with \c dbnextrow().  Possible values are:
 * -  0 \a column bound successfully
 * - -1 \a column is NULL.
 * - >0 true length of data, had \a column not been truncated due to insufficient space in the columns bound host variable .  
 * \sa dbanullbind(), dbbind(), dbdata(), dbdatlen(), dbnextrow().
 */
RETCODE
dbnullbind(DBPROCESS * dbproc, int column, DBINT * indicator)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbnullbind(%p, %d, %p)\n", dbproc, column, indicator);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return FAIL; /* dbcolptr sent SYBECNOR, Column number out of range */

	colinfo->column_nullbind = (TDS_SMALLINT *)indicator;
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Tie a null-indicator to a compute result column.
 * 
 * 
 * When a row is fetched, the indicator variable tells the state of the column's data.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid identifies which one of potientially many compute rows is meant. The first compute
 * clause has \a computeid == 1.  
 * \param column Nth column in the result set, starting with 1.
 * \param indicator address of host variable.
 * \retval SUCCEED variable accepted.
 * \retval FAIL \a indicator is NULL or \a column is out of range. 
 * \remarks Contents of \a indicator are set with \c dbnextrow().  Possible values are:
 * -  0 \a column bound successfully
 * - -1 \a column is NULL.
 * - >0 true length of data, had \a column not been truncated due to insufficient space in the columns bound host variable .  
 * \sa dbadata(), dbadlen(), dbaltbind(), dbnextrow(), dbnullbind().
 * \todo Never fails, but only because failure conditions aren't checked.  
 */
RETCODE
dbanullbind(DBPROCESS * dbproc, int computeid, int column, DBINT * indicator)
{
	TDSCOLUMN *curcol;

	tdsdump_log(TDS_DBG_FUNC, "dbanullbind(%p, %d, %d, %p)\n", dbproc, computeid, column, indicator);

	curcol = dbacolptr(dbproc, computeid, column, 1);
	if (!curcol)
		return FAIL;

	/*
	 *  XXX Need to check for possibly problems before assuming
	 *  everything is okay
	 */
	curcol->column_nullbind = (TDS_SMALLINT *)indicator;

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Indicates whether or not the count returned by dbcount is real (Microsoft-compatibility feature).
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \returns TRUE if the count returned by dbcount is real or FALSE if the count returned by dbcount is not real.
 * \sa DBCOUNT(), dbcount().
 */
BOOL
dbiscount(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbiscount(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	return dbproc->tds_socket && dbproc->tds_socket->rows_affected != TDS_NO_COUNT;
}

/**
 * \ingroup dblib_core
 * \brief Get count of rows processed
 *
 *
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \returns
 * 	- for insert/update/delete, count of rows affected.
 * 	- for select, count of rows returned, after all rows have been fetched.
 * \sa DBCOUNT(), dbnextrow(), dbresults().
 */
DBINT
dbcount(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbcount(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	if (!dbproc || !dbproc->tds_socket || dbproc->tds_socket->rows_affected == TDS_NO_COUNT)
		return -1;
	return (DBINT)dbproc->tds_socket->rows_affected;
}

/**
 * \ingroup dblib_core
 * \brief Clear \a n rows from the row buffer.
 * 
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server. 
 * \param n number of rows to remove, >= 0.
 * \sa dbgetrow(), dbnextrow(), dbsetopt().
 */
void
dbclrbuf(DBPROCESS * dbproc, DBINT n)
{
	tdsdump_log(TDS_DBG_FUNC, "dbclrbuf(%p, %d)\n", dbproc, n);
	CHECK_PARAMETER(dbproc, SYBENULL, );

	if (n <= 0)
		return;

	if (dbproc->dbopts[DBBUFFER].factive) {
		DBPROC_ROWBUF * buf = &(dbproc->row_buf);
		int count = buffer_count(buf);
		if (n >= count)
			n = count - 1;
		buffer_delete_rows(&(dbproc->row_buf), n);
	}
}

/**
 * \ingroup dblib_core
 * \brief Test whether or not a datatype can be converted to another datatype
 * 
 * \param srctype type converting from
 * \param desttype type converting to
 * \remarks dbwillconvert() lies sometimes.  Some datatypes \em should be convertible but aren't yet in our implementation.  
 *          Legal unimplemented conversions return \em TRUE.  
 * \retval TRUE convertible, or should be.
 * \retval FAIL not convertible.  
 * \sa dbaltbind(), dbbind(), dbconvert(), dbconvert_ps(), \c src/dblib/unittests/convert().c().
 */
DBBOOL
dbwillconvert(int srctype, int desttype)
{
	tdsdump_log(TDS_DBG_FUNC, "dbwillconvert(%s, %s)\n", tds_prdatatype(srctype), tds_prdatatype(desttype));
	return tds_willconvert(srctype, desttype) ? TRUE : FALSE;
}

/**
 * \ingroup dblib_core
 * \brief Get the datatype of a regular result set column. 
 * 
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \returns \c SYB* datetype token value, or zero if \a column out of range
 * \sa dbcollen(), dbcolname(), dbdata(), dbdatlen(), dbnumcols(), dbprtype(), dbvarylen().
 */
int
dbcoltype(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbcoltype(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return -1;

	switch (colinfo->column_type) {
	case SYBVARCHAR:
		return SYBCHAR;
	case SYBVARBINARY:
		return SYBBINARY;
	}
	return tds_get_conversion_type(colinfo->column_type, colinfo->column_size);
}

/**
 * \ingroup dblib_core
 * \brief Get user-defined datatype of a regular result column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \returns \c SYB* datetype token value, or -1 if \a column out of range
 * \sa dbaltutype(), dbcoltype().
 */
int
dbcolutype(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbcolutype(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return -1;

	return colinfo->column_usertype;
}

/**
 * \ingroup dblib_core
 * \brief Get precision and scale information for a regular result column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \return Pointer to a DBTYPEINFO structure .  NULL \a column is out of range.
 * \sa dbcollen(), dbcolname(), dbcoltype(), dbdata(), dbdatlen(), dbnumcols(), dbprtype(), dbvarylen().
 */
DBTYPEINFO *
dbcoltypeinfo(DBPROCESS * dbproc, int column)
{
	/* moved typeinfo from static into dbproc structure to make thread safe.  (mlilback 11/7/01) */
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbcoltypeinfo(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return NULL;

	dbproc->typeinfo.precision = colinfo->column_prec;
	dbproc->typeinfo.scale = colinfo->column_scale;
	return &dbproc->typeinfo;
}

/**
 * \brief Get a bunch of column attributes with a single call (Microsoft-compatibility feature).  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param type must be CI_REGULAR or CI_ALTERNATE (CI_CURSOR is defined by the vendor, but is not yet implemented).
 * \param column Nth in the result set, starting from 1.
 * \param computeid (ignored)
 * \param pdbcol address of structure to be populated by this function.  
 * \return SUCCEED or FAIL. 
 * \sa dbcolbrowse(), dbqual(), dbtabbrowse(), dbtabcount(), dbtabname(), dbtabsource(), dbtsnewlen(), dbtsnewval(), dbtsput().
 * \todo Support cursor rows. 
 */
RETCODE
dbcolinfo (DBPROCESS *dbproc, CI_TYPE type, DBINT column, DBINT computeid, DBCOL *pdbcol )
{
	DBTYPEINFO *ps;
	TDSCOMPUTEINFO *info;
	TDSCOLUMN *colinfo;
	int i;

	tdsdump_log(TDS_DBG_FUNC, "dbcolinfo(%p, %d, %d, %d, %p)\n", dbproc, type, column, computeid, pdbcol);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return FAIL;
		
	CHECK_NULP(pdbcol, "dbcolinfo", 5, FAIL);

	if (type == CI_REGULAR) {

		strlcpy(pdbcol->Name, dbcolname(dbproc, column), sizeof(pdbcol->Name));
		strlcpy(pdbcol->ActualName, dbcolname(dbproc, column), sizeof(pdbcol->ActualName));

		pdbcol->Type = dbcoltype(dbproc, column);
		pdbcol->UserType = dbcolutype(dbproc, column);
		pdbcol->MaxLength = dbcollen(dbproc, column);
		pdbcol->Null = _dbnullable(dbproc, column);
		pdbcol->VarLength = dbvarylen(dbproc, column);

		ps = dbcoltypeinfo(dbproc, column);

		if( ps ) {
			pdbcol->Precision = ps->precision;
			pdbcol->Scale = ps->scale;
		}

		pdbcol->Updatable = colinfo->column_writeable ? TRUE : FALSE;
		pdbcol->Identity = colinfo->column_identity ? TRUE : FALSE;

		return SUCCEED;
	}
  
	if (type == CI_ALTERNATE) {

		if (computeid == 0)
			return FAIL;

		for (i = 0;; ++i) {
			if (i >= dbproc->tds_socket->num_comp_info)
				return FAIL;
			info = dbproc->tds_socket->comp_info[i];
			if (info->computeid == computeid)
				break;
		}

		/* if either the compute id or the column number are invalid, return -1 */
		if (column < 1 || column > info->num_cols)
			return FAIL;

		colinfo = info->columns[column - 1];

		strlcpy(pdbcol->Name, tds_dstr_cstr(&colinfo->column_name), sizeof(pdbcol->Name));
		strlcpy(pdbcol->ActualName, tds_dstr_cstr(&colinfo->column_name), sizeof(pdbcol->ActualName));

		pdbcol->Type = dbalttype(dbproc, computeid, column);
		pdbcol->UserType = dbaltutype(dbproc, computeid, column);
		pdbcol->MaxLength = dbaltlen(dbproc, computeid, column);
		if (colinfo->column_nullable) 
			pdbcol->Null = TRUE;
		else
			pdbcol->Null = FALSE;

		pdbcol->VarLength = FALSE;

		if (colinfo->column_nullable
		    || is_nullable_type(colinfo->column_type))
			pdbcol->VarLength = TRUE;

		pdbcol->Precision = colinfo->column_prec;
		pdbcol->Scale = colinfo->column_scale;
  
		pdbcol->Updatable = colinfo->column_writeable ? TRUE : FALSE ;
		pdbcol->Identity = colinfo->column_identity ? TRUE : FALSE ;

		return SUCCEED;
	}

	return FAIL;
}
 
/**
 * \ingroup dblib_core
 * \brief Get base database column name for a result set column.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \return pointer to ASCII null-terminated string, the name of the column. On error, NULL.
 * \sa dbcolbrowse(), dbqual(), dbtabbrowse(), dbtabcount(), dbtabname(), dbtabsource(), dbtsnewlen(), dbtsnewval(), dbtsput().
 */
char *
dbcolsource(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbcolsource(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return NULL;

	return tds_dstr_buf(tds_dstr_isempty(&colinfo->table_column_name) ?
		&colinfo->column_name :
		&colinfo->table_column_name);
}

/**
 * \ingroup dblib_core
 * \brief Get size of a regular result column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \return size of the column (not of data in any particular row).  On error, -1.
 * \sa dbcolname(), dbcoltype(), dbdata(), dbdatlen(), dbnumcols().
 */
DBINT
dbcollen(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbcollen(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return -1;

	return colinfo->column_size;
}

/**
 * \ingroup dblib_core
 * \brief Get size of a result column needed to print column.
 *
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \return size of the column in characters (not of data in any particular row).  On error, -1.
 * \sa dbcolname(), dbcoltype(), dbdata(), dbdatlen(), dbnumcols().
 */
DBINT
dbprcollen(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbprcollen(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return 0;

	return _get_printable_size(colinfo);
}


/* dbvarylen(), pkleef@openlinksw.com 01/21/02 */
/**
 * \ingroup dblib_core
 * \brief Determine whether a column can vary in size.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \retval TRUE datatype of column can vary in size, or is nullable. 
 * \retval FALSE datatype of column is fixed and is not nullable. 
 * \sa dbcollen(), dbcolname(), dbcoltype(), dbdata(), dbdatlen(), dbnumcols(), dbprtype().
 */
DBINT
dbvarylen(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbvarylen(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return FALSE;

	if (colinfo->column_nullable)
		return TRUE;

	switch (colinfo->column_type) {
		/* variable length fields */
	case SYBNVARCHAR:
	case SYBVARBINARY:
	case SYBVARCHAR:
		return TRUE;

		/* types that can be null */
	case SYBBITN:
	case SYBDATETIMN:
	case SYBDECIMAL:
	case SYBFLTN:
	case SYBINTN:
	case SYBMONEYN:
	case SYBNUMERIC:
		return TRUE;

		/* blob types */
	case SYBIMAGE:
	case SYBNTEXT:
	case SYBTEXT:
		return TRUE;
	}
	return FALSE;
}

/**
 * \ingroup dblib_core
 * \brief   Get size of current row's data in a regular result column.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \return size of the data, in bytes.
 * \sa dbcollen(), dbcolname(), dbcoltype(), dbdata(), dbnumcols().
 */
DBINT
dbdatlen(DBPROCESS * dbproc, int column)
{
	DBINT len;
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbdatlen(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return -1;	

	len = (colinfo->column_cur_size < 0)? 0 : colinfo->column_cur_size;

	tdsdump_log(TDS_DBG_FUNC, "dbdatlen() type = %d, len= %d\n", colinfo->column_type, len);

	return len;
}

/**
 * \ingroup dblib_core
 * \brief Get address of data in a regular result column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \return pointer the data, or NULL if data are NULL, or if \a column is out of range.  
 * \sa dbbind(), dbcollen(), dbcolname(), dbcoltype(), dbdatlen(), dbnumcols().
 */
BYTE *
dbdata(DBPROCESS * dbproc, int column)
{
	tdsdump_log(TDS_DBG_FUNC, "dbdata(%p, %d)\n", dbproc, column);

	return _dbcoldata(dbcolptr(dbproc, column));
}

/** \internal
 * \ingroup dblib_internal
 * \brief Return data from a column
 *
 * \param colinfo contains information on a result column.
 * \return pointer to the data, or NULL if data are NULL
 * \sa dbdata(), dbretdata()
 */
static BYTE *
_dbcoldata(TDSCOLUMN *colinfo)
{
	BYTE *res;
	const static BYTE empty[1] = { 0 };

	if (!colinfo || colinfo->column_cur_size < 0)
		return NULL;

	res = colinfo->column_data;
	if (is_blob_col(colinfo))
		res = (BYTE *) ((TDSBLOB *) res)->textvalue;
	if (!res)
		return (BYTE *) empty;
	return res;
}

/**
 * \ingroup dblib_core
 * \brief Cancel the current command batch.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval SUCCEED always.
 * \sa dbcanquery(), dbnextrow(), dbresults(), dbsetinterrupt(), dbsqlexec(), dbsqlok(), dbsqlsend().
 * \todo Check for failure and return accordingly.
 */
RETCODE
dbcancel(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;

	tdsdump_log(TDS_DBG_FUNC, "dbcancel(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	tds = dbproc->tds_socket;

	tds_send_cancel(tds);
	tds_process_cancel(tds);

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Determine size buffer required to hold the results returned by dbsprhead(), dbsprline(), and  dbspr1row().
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return size of buffer requirement, in bytes.  
 * \remarks An esoteric function.
 * \sa dbprhead(), dbprrow(), dbspr1row(), dbsprhead(), dbsprline().
 */
DBINT
dbspr1rowlen(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;
	int col, len = 0;

	tdsdump_log(TDS_DBG_FUNC, "dbspr1rowlen(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);
	CHECK_PARAMETER(dbproc->tds_socket, SYBEDDNE, 0);

	tds = dbproc->tds_socket;
	
	for (col = 0; col < tds->res_info->num_cols; col++) {
		TDSCOLUMN *colinfo = tds->res_info->columns[col];
		int collen = _get_printable_size(colinfo);
		int namlen = tds_dstr_len(&colinfo->column_name);
		
		len += collen > namlen ? collen : namlen;
		
		if (col > 0) 	/* allow for the space between columns */
			len += dbstring_length(dbproc->dbopts[DBPRCOLSEP].param);
	}
	
	return ++len; 	/* allow for the nul */
}

/**
 * \ingroup dblib_core
 * \brief Print a regular result row to a buffer.  
 * 
 * Fills a buffer with one data row, represented as a null-terminated ASCII string.  Helpful for debugging.  
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param buffer \em output: Address of a buffer to hold ASCII null-terminated string.
 * \param buf_len size of \a buffer, in bytes. 
 * \retval SUCCEED on success.
 * \retval FAIL trouble encountered.  
 * \sa dbclropt(), dbisopt(), dbprhead(), dbprrow(), dbspr1rowlen(), dbsprhead(), dbsprline().
 */
RETCODE
dbspr1row(DBPROCESS * dbproc, char *buffer, DBINT buf_len)
{
	TDSSOCKET *tds;
	TDSDATEREC when;
	int i, c, col;
	DBINT len;

	tdsdump_log(TDS_DBG_FUNC, "dbspr1row(%p, %s, %d)\n", dbproc, buffer, buf_len);
	CHECK_CONN(FAIL);
	CHECK_NULP(buffer, "dbspr1row", 2, FAIL);

	if (!dbproc->tds_socket)
		return FAIL;

	tds = dbproc->tds_socket;

	for (col = 0; col < tds->res_info->num_cols; col++) {
		int padlen, collen, namlen;
		TDSCOLUMN *colinfo = tds->res_info->columns[col];
		if (colinfo->column_cur_size < 0) {
			len = 4;
			if (buf_len <= len) {
				return FAIL;
			}
			strcpy(buffer, "NULL");
		} else {
			TDS_SERVER_TYPE desttype, srctype;

			desttype = dblib_bound_type(STRINGBIND);
			srctype = tds_get_conversion_type(colinfo->column_type, colinfo->column_size);
			if (is_datetime_type(srctype)) {
				tds_datecrack(srctype, dbdata(dbproc, col + 1), &when);
				len = (int)tds_strftime(buffer, buf_len, STD_DATETIME_FMT, &when, 3);
			} else {
				len = dbconvert(dbproc, srctype, dbdata(dbproc, col + 1), dbdatlen(dbproc, col + 1), 
						desttype, (BYTE *) buffer, buf_len);
			}
			if (len == -1) {
				return FAIL;
			}
		}
		buffer += len;
		buf_len -= len;
		collen = _get_printable_size(colinfo);
		namlen = tds_dstr_len(&colinfo->column_name);
		padlen = (collen > namlen ? collen : namlen) - len;
		if ((c = dbstring_getchar(dbproc->dbopts[DBPRPAD].param, 0)) == -1) {
			c = ' ';
		}
		for (; padlen > 0; padlen--) {
			if (buf_len < 1) {
				return FAIL;
			}
			*buffer++ = c;
			buf_len--;
		}
		if ((col + 1) < tds->res_info->num_cols) {
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i)) != -1) {
				if (buf_len < 1) {
					return FAIL;
				}
				*buffer++ = c;
				buf_len--;
				i++;
			}
		}
	}
	if (buf_len < 1) {
		return FAIL;
	}
	*buffer = '\0';
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Print a result set to stdout. 
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa dbbind(), dbnextrow(), dbprhead(), dbresults(), dbspr1row(), dbsprhead(), dbsprline(). 
 */
RETCODE
dbprrow(DBPROCESS * dbproc)
{
	TDSCOLUMN *colinfo;
	TDSRESULTINFO *resinfo;
	TDSSOCKET *tds;
	int i, col, collen, namlen, len;
	char dest[8192];
	int desttype, srctype;
	TDSDATEREC when;
	STATUS status;
	int padlen;
	int c;
	int selcol;
	int linechar;
	int op;
	const char *opname;

	/* these are for compute rows */
	DBINT computeid, num_cols, colid;
	TDS_SMALLINT *col_printlens = NULL;

	tdsdump_log(TDS_DBG_FUNC, "dbprrow(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	tds = dbproc->tds_socket;

	while ((status = dbnextrow(dbproc)) != NO_MORE_ROWS) {

		if (status == FAIL) {
			free(col_printlens);
			return FAIL;
		}

		if (status == REG_ROW) {

			resinfo = tds->res_info;

			if (col_printlens == NULL) {
				if ((col_printlens = tds_new0(TDS_SMALLINT, resinfo->num_cols)) == NULL) {
					dbperror(dbproc, SYBEMEM, errno);
					return FAIL;
				}
			}

			for (col = 0; col < resinfo->num_cols; col++) {
				colinfo = resinfo->columns[col];
				if (colinfo->column_cur_size < 0) {
					len = 4;
					strcpy(dest, "NULL");
				} else {
					desttype = dblib_bound_type(STRINGBIND);
					srctype = tds_get_conversion_type(colinfo->column_type, colinfo->column_size);
					if (is_datetime_type(srctype)) {
						tds_datecrack(srctype, dbdata(dbproc, col + 1), &when);
						len = (int)tds_strftime(dest, sizeof(dest), STD_DATETIME_FMT, &when, 3);
					} else {
						len = dbconvert(dbproc, srctype, dbdata(dbproc, col + 1), dbdatlen(dbproc, col + 1),
								desttype, (BYTE *) dest, sizeof(dest));
					}
				}

				printf("%.*s", len, dest);
				collen = _get_printable_size(colinfo);
				namlen = tds_dstr_len(&colinfo->column_name);
				padlen = (collen > namlen ? collen : namlen) - len;

				c = dbstring_getchar(dbproc->dbopts[DBPRPAD].param, 0);
				for (; c > -1 && padlen > 0; padlen--) {
					putchar(c);
				}

				if ((col + 1) < resinfo->num_cols) {
					i = 0;
					while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i++)) != -1) {
						putchar(c);
					}
				}
				col_printlens[col] = collen;
			}
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRLINESEP].param, i++)) != -1) {
				putchar(c);
			}

		} else {

			computeid = status;

			for (i = 0;; ++i) {
				if (i >= tds->num_comp_info) {
					free(col_printlens);
					return FAIL;
				}
				resinfo = tds->comp_info[i];
				if (resinfo->computeid == computeid)
					break;
			}

			num_cols = dbnumalts(dbproc, computeid);
			tdsdump_log(TDS_DBG_FUNC, "dbprrow num compute cols = %d\n", num_cols);

			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRLINESEP].param, i++)) != -1) {
				putchar(c);
			}
			for (selcol = col = 1; col <= num_cols; col++) {
				tdsdump_log(TDS_DBG_FUNC, "dbprrow calling dbaltcolid(%d,%d)\n", computeid, col);
				colid = dbaltcolid(dbproc, computeid, col);
				/*
				 * The pad character is pointed to by dbopts[DBPRPAD].param.  If that pointer 
				 * is NULL -- meaning padding is turned off -- dbstring_getchar returns -1. 
				 */
				while (selcol < colid) {
					for (i = 0; i < col_printlens[selcol - 1]; i++) {
						if ((c = dbstring_getchar(dbproc->dbopts[DBPRPAD].param, 0)) >= 0)
							putchar(c); 
					}
					selcol++;
					i = 0;
					while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i++)) != -1) {
						putchar(c);
					}
				}
				op = dbaltop(dbproc, computeid, col);
				opname = dbprtype(op);
				printf("%s", opname);
				for (i = 0; i < ((long) col_printlens[selcol - 1] - (long) strlen(opname)); i++) {
					if ((c = dbstring_getchar(dbproc->dbopts[DBPRPAD].param, 0)) >= 0)
						putchar(c); 
				}
				selcol++;
				if ((colid + 1) < num_cols) {
					i = 0;
					while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i++)) != -1) {
						putchar(c);
					}
				}
			}
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRLINESEP].param, i++)) != -1) {
				putchar(c);
			}

			for (selcol = col = 1; col <= num_cols; col++) {
				tdsdump_log(TDS_DBG_FUNC, "dbprrow calling dbaltcolid(%d,%d)\n", computeid, col);
				colid = dbaltcolid(dbproc, computeid, col);
				while (selcol < colid) {
					for (i = 0; i < col_printlens[selcol - 1]; i++) {
						putchar(' ');
					}
					selcol++;
					i = 0;
					while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i++)) != -1) {
						putchar(c);
					}
				}
				if (resinfo->by_cols > 0) {
					linechar = '-';
				} else {
					linechar = '=';
				}
				for (i = 0; i < col_printlens[colid - 1]; i++)
					putchar(linechar);
				selcol++;
				if ((colid + 1) < num_cols) {
					i = 0;
					while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i++)) != -1) {
						putchar(c);
					}
				}
			}
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRLINESEP].param, i++)) != -1) {
				putchar(c);
			}

			for (selcol = col = 1; col <= num_cols; col++) {
				colinfo = resinfo->columns[col - 1];

				desttype = dblib_bound_type(STRINGBIND);
				srctype = dbalttype(dbproc, computeid, col);

				if (is_datetime_type(srctype)) {
					tds_datecrack(srctype, dbadata(dbproc, computeid, col), &when);
					len = (int)tds_strftime(dest, sizeof(dest), STD_DATETIME_FMT, &when, 3);
				} else {
					len = dbconvert(dbproc, srctype, dbadata(dbproc, computeid, col), -1, desttype,
							(BYTE *) dest, sizeof(dest));
				}

				tdsdump_log(TDS_DBG_FUNC, "dbprrow calling dbaltcolid(%d,%d)\n", computeid, col);
				colid = dbaltcolid(dbproc, computeid, col);
				tdsdump_log(TDS_DBG_FUNC, "dbprrow select column = %d\n", colid);

				while (selcol < colid) {
					for (i = 0; i < col_printlens[selcol - 1]; i++) {
						putchar(' ');
					}
					selcol++;
					i = 0;
					while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i++)) != -1) {
						putchar(c);
					}
				}
				printf("%.*s", len, dest);
				collen = _get_printable_size(colinfo);
				namlen = tds_dstr_len(&colinfo->column_name);
				padlen = (collen > namlen ? collen : namlen) - len;
				if ((c = dbstring_getchar(dbproc->dbopts[DBPRPAD].param, 0)) == -1) {
					c = ' ';
				}
				for (; padlen > 0; padlen--) {
					putchar(c);
				}
				selcol++;
				if ((colid + 1) < num_cols) {
					i = 0;
					while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i++)) != -1) {
						putchar(c);
					}
				}
			}
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRLINESEP].param, i++)) != -1) {
				putchar(c);
			}
		}
	}

	free(col_printlens);

	return SUCCEED;
}

static int
_get_printable_size(TDSCOLUMN * colinfo)
{
	switch (tds_get_conversion_type(colinfo->column_type, colinfo->column_size)) {
	case SYBINT1:
		return 3;
	case SYBINT2:
		return 6;
	case SYBINT4:
		return 11;
	case SYBINT8:
		return 21;
	case SYBVARCHAR:
	case SYBCHAR:
	case SYBTEXT:
	case SYBNTEXT:
	case SYBNVARCHAR:
	case SYBLONGCHAR:
		return colinfo->column_size;
	case SYBBINARY:
	case SYBIMAGE:
	case SYBLONGBINARY:
	case SYBVARBINARY:
		return colinfo->column_size * 2u;
	case SYBFLT8:
	case SYBREAL:
		return 11;	/* FIX ME -- we do not track precision */
	case SYBMONEY4:
		return 12;
	case SYBMONEY:
		return 22;
	case SYB5BIGDATETIME:
	case SYBDATETIME:
	case SYBDATETIME4:
		return 26;
	case SYBTIME:
	case SYB5BIGTIME:
		return 15;
	case SYBDATE:
		return 10;
	case SYBUNIQUE:
		return 36;
	case SYBBIT:
		return 1;
		/* FIX ME -- not all types present */
	default:
		return 0;
	}
}

/**
 * \ingroup dblib_core
 * \brief Get formatted string for underlining dbsprhead() column names.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param buffer output buffer
 * \param buf_len size of \a buffer
 * \param line_char character to use to represent underlining.
 * \retval SUCCEED \a buffer filled.
 * \retval FAIL insufficient space in \a buffer, usually.
 * \sa dbprhead(), dbprrow(), dbspr1row(), dbspr1rowlen(), dbsprhead(). 
 */
RETCODE
dbsprline(DBPROCESS * dbproc, char *buffer, DBINT buf_len, DBCHAR line_char)
{
	TDSCOLUMN *colinfo;
	TDSRESULTINFO *resinfo;
	TDSSOCKET *tds;
	int i, col, len, collen, namlen;
	int c;

	tdsdump_log(TDS_DBG_FUNC, "dbsprline(%p, %s, %d, '%c')\n", dbproc, buffer, buf_len, line_char);
	CHECK_CONN(FAIL);
	CHECK_NULP(buffer, "dbsprline", 2, FAIL);

	tds = dbproc->tds_socket;
	resinfo = tds->res_info;

	for (col = 0; col < resinfo->num_cols; col++) {
		colinfo = resinfo->columns[col];
		collen = _get_printable_size(colinfo);
		namlen = tds_dstr_len(&colinfo->column_name);
		len = collen > namlen ? collen : namlen;
		for (i = 0; i < len; i++) {
			if (buf_len < 1) {
				return FAIL;
			}
			*buffer++ = line_char;
			buf_len--;
		}
		if ((col + 1) < resinfo->num_cols) {
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i)) != -1) {
				if (buf_len < 1) {
					return FAIL;
				}
				*buffer++ = c;
				buf_len--;
				i++;
			}
		}
	}
	if (buf_len < 1) {
		return FAIL;
	}
	*buffer = '\0';
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Print result set headings to a buffer.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param buffer output buffer
 * \param buf_len size of \a buffer
 * \retval SUCCEED \a buffer filled.
 * \retval FAIL insufficient spaace in \a buffer, usually.
 * \sa dbprhead(), dbprrow(), dbsetopt(), dbspr1row(), dbspr1rowlen(), dbsprline().
 */
RETCODE
dbsprhead(DBPROCESS * dbproc, char *buffer, DBINT buf_len)
{
	TDSCOLUMN *colinfo;
	TDSRESULTINFO *resinfo;
	TDSSOCKET *tds;
	int i, col, collen, namlen;
	int padlen;
	int c;

	tdsdump_log(TDS_DBG_FUNC, "dbsprhead(%p, %p, %d)\n", dbproc, buffer, buf_len);
	CHECK_CONN(FAIL);
	CHECK_NULP(buffer, "dbsprhead", 2, FAIL);

	tds = dbproc->tds_socket;
	resinfo = tds->res_info;

	for (col = 0; col < resinfo->num_cols; col++) {
		colinfo = resinfo->columns[col];
		collen = _get_printable_size(colinfo);
		namlen = tds_dstr_len(&colinfo->column_name);
		padlen = (collen > namlen ? collen : namlen) - namlen;
		if (buf_len < namlen) {
			return FAIL;
		}
		memcpy(buffer, tds_dstr_cstr(&colinfo->column_name), namlen);
		buffer += namlen;
		buf_len -= namlen;
		if ((c = dbstring_getchar(dbproc->dbopts[DBPRPAD].param, 0)) == -1) {
			c = ' ';
		}
		for (; padlen > 0; padlen--) {
			if (buf_len < 1) {
				return FAIL;
			}
			*buffer++ = c;
			buf_len--;
		}
		if ((col + 1) < resinfo->num_cols) {
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i)) != -1) {
				if (buf_len < 1) {
					return FAIL;
				}
				*buffer++ = c;
				buf_len--;
				i++;
			}
		}
	}
	if (buf_len < 1) {
		return FAIL;
	}
	*buffer = '\0';
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Print result set headings to stdout.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa 
 */
void
dbprhead(DBPROCESS * dbproc)
{
	TDSCOLUMN *colinfo;
	TDSRESULTINFO *resinfo;
	TDSSOCKET *tds;
	int i, col, len, collen, namlen;
	int padlen;
	int c;

	tdsdump_log(TDS_DBG_FUNC, "dbprhead(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, );

	tds = dbproc->tds_socket;
	resinfo = tds->res_info;
	if (resinfo == NULL) {
		return;
	}
	for (col = 0; col < resinfo->num_cols; col++) {
		colinfo = resinfo->columns[col];
		collen = _get_printable_size(colinfo);
		namlen = tds_dstr_len(&colinfo->column_name);
		padlen = (collen > namlen ? collen : namlen) - namlen;
		printf("%s", tds_dstr_cstr(&colinfo->column_name));

		c = dbstring_getchar(dbproc->dbopts[DBPRPAD].param, 0);
		if (c == -1) {
			c = ' ';
		}
		for (; padlen > 0; padlen--) {
			putchar(c);
		}

		if ((col + 1) < resinfo->num_cols) {
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i)) != -1) {
				putchar(c);
				i++;
			}
		}
	}
	i = 0;
	while ((c = dbstring_getchar(dbproc->dbopts[DBPRLINESEP].param, i)) != -1) {
		putchar(c);
		i++;
	}
	for (col = 0; col < resinfo->num_cols; col++) {
		colinfo = resinfo->columns[col];
		collen = _get_printable_size(colinfo);
		namlen = tds_dstr_len(&colinfo->column_name);
		len = collen > namlen ? collen : namlen;
		for (i = 0; i < len; i++)
			putchar('-');
		if ((col + 1) < resinfo->num_cols) {
			i = 0;
			while ((c = dbstring_getchar(dbproc->dbopts[DBPRCOLSEP].param, i)) != -1) {
				putchar(c);
				i++;
			}
		}
	}
	i = 0;
	while ((c = dbstring_getchar(dbproc->dbopts[DBPRLINESEP].param, i)) != -1) {
		putchar(c);
		i++;
	}
}

/** \internal
 * \ingroup dblib_internal
 * \brief Indicate whether a query returned rows.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa DBROWS(), DBCMDROW(), dbnextrow(), dbresults(), DBROWTYPE().
 */
RETCODE
dbrows(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;

	tdsdump_log(TDS_DBG_FUNC, "dbrows(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	if (!(tds=dbproc->tds_socket))
		return FAIL;

	return (tds->res_info && tds->res_info->rows_exist)? SUCCEED : FAIL;
}

#if defined(DBLIB_UNIMPLEMENTED)
/**
 * \ingroup dblib_core
 * \brief Set the default character set for an application.
 * 
 * \param language ASCII null-terminated string.  
 * \sa dbsetdeflang(), dbsetdefcharset(), dblogin(), dbopen().
 * \retval SUCCEED Always.  
 * \todo Unimplemented.
 */
RETCODE
dbsetdeflang(char *language)
{
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbsetdeflang(%s)\n", language);
	CHECK_PARAMETER_NOPROC(language, SYBENULP);
	return SUCCEED;
}
#endif

/**
 * \ingroup dblib_core
 * \brief Get TDS packet size for the connection.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return TDS packet size, in bytes.  
 * \sa DBSETLPACKET()
 */
int
dbgetpacket(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;

	tdsdump_log(TDS_DBG_FUNC, "dbgetpacket(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, TDS_DEF_BLKSZ);

	tds = dbproc->tds_socket;
	if (!tds) {
		return TDS_DEF_BLKSZ;
	} else {
		return tds->conn->env.block_size;
	}
}

/**
 * \ingroup dblib_core
 * \brief Set maximum simultaneous connections db-lib will open to the server.
 * 
 * \param maxprocs Limit for process.
 * \retval SUCCEED Always.  
 * \sa dbgetmaxprocs(), dbopen()
 */
RETCODE
dbsetmaxprocs(int maxprocs)
{
	int i, j;
	TDSSOCKET **old_list;

	tdsdump_log(TDS_DBG_FUNC, "UNTESTED dbsetmaxprocs(%d)\n", maxprocs);

	/* not too few elements */
	if (maxprocs <= 0)
		return FAIL;

	tds_mutex_lock(&dblib_mutex);

	old_list = g_dblib_ctx.connection_list;

	/* "compress" array */
	for (i = 0; i < g_dblib_ctx.connection_list_size; ++i) {
		/* if empty replace with first no-empty */
		if (old_list[i])
			continue;
		for (j = i + 1; j < g_dblib_ctx.connection_list_size; ++j)
			if (old_list[j]) {
				old_list[i] = old_list[j];
				old_list[j] = NULL;
				break;
			}
		if (j >= g_dblib_ctx.connection_list_size)
			break;
	}
	/* do not restrict too much, i here contains minimun size */
	if (maxprocs < i)
		maxprocs = i;

	/*
	 * Don't reallocate less memory.  
	 * If maxprocs is less than was initially allocated, just reduce the represented list size.  
	 * If larger, reallocate and copy.
	 * We probably should check for valid connections beyond the new max.
	 */
	if (maxprocs <= g_dblib_ctx.connection_list_size) {
		g_dblib_ctx.connection_list_size_represented = maxprocs;
		tds_mutex_unlock(&dblib_mutex);
		return SUCCEED;
	}

	g_dblib_ctx.connection_list = tds_new0(TDSSOCKET *, maxprocs);

	if (g_dblib_ctx.connection_list == NULL) {
		g_dblib_ctx.connection_list = old_list;
		tds_mutex_unlock(&dblib_mutex);
		dbperror(NULL, SYBEMEM, errno);
		return FAIL;
	}

	for (i = 0; i < g_dblib_ctx.connection_list_size; i++) {
		g_dblib_ctx.connection_list[i] = old_list[i];
	}

	g_dblib_ctx.connection_list_size = maxprocs;
	g_dblib_ctx.connection_list_size_represented = maxprocs;

	tds_mutex_unlock(&dblib_mutex);

	free(old_list);

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief get maximum simultaneous connections db-lib will open to the server.
 * 
 * \return Current maximum.  
 * \sa dbsetmaxprocs(), dbopen()
 */
int
dbgetmaxprocs(void)
{
	int r;

	tdsdump_log(TDS_DBG_FUNC, "dbgetmaxprocs(void)\n");

	tds_mutex_lock(&dblib_mutex);
	r = g_dblib_ctx.connection_list_size_represented;
	tds_mutex_unlock(&dblib_mutex);
	return r;
}

/**
 * \ingroup dblib_core
 * \brief Set maximum seconds db-lib waits for a server response to query.  
 * 
 * \param seconds New limit for application.  
 * \retval SUCCEED Always.  
 * \sa dberrhandle(), DBGETTIME(), dbsetlogintime(), dbsqlexec(), dbsqlok(), dbsqlsend().
 */
RETCODE
dbsettime(int seconds)
{
	TDSSOCKET **tds;
	int i;
	DBPROCESS *dbproc;
	tdsdump_log(TDS_DBG_FUNC, "dbsettime(%d)\n", seconds);

	tds_mutex_lock(&dblib_mutex);
	g_dblib_ctx.query_timeout = seconds;
	
	tds = g_dblib_ctx.connection_list;
	for (i = 0; i <  TDS_MAX_CONN; i++) {
		if (tds[i]) {
			dbproc = (DBPROCESS *) tds_get_parent(tds[i]);
			if (!dbisopt(dbproc, DBSETTIME, 0))
				tds[i]->query_timeout = seconds;
		}
	}
	
	tds_mutex_unlock(&dblib_mutex);
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Get maximum seconds db-lib waits for a server response to query.  
 * 
 * \retval query timeout limit, in seconds
 * \sa dberrhandle(), DBSETTIME(), dbsetlogintime(), dbsqlexec(), dbsqlok(), dbsqlsend().
 */
int
dbgettime(void)
{
	tdsdump_log(TDS_DBG_FUNC, "dbgettime()\n");

	return g_dblib_ctx.query_timeout;
}

/**
 * \ingroup dblib_core
 * \brief Set maximum seconds db-lib waits for a server response to a login attempt.  
 * 
 * \param seconds New limit for application.  
 * \retval SUCCEED Always.  
 * \sa dberrhandle(), dbsettime()
 */
RETCODE
dbsetlogintime(int seconds)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetlogintime(%d)\n", seconds);

	tds_mutex_lock(&dblib_mutex);
	g_dblib_ctx.login_timeout = seconds;
	tds_mutex_unlock(&dblib_mutex);
	return SUCCEED;
}

/** \internal
 * \ingroup dblib_internal
 * \brief See if the current command can return rows.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval SUCCEED Yes, it can.  
 * \retval FAIL No, it can't.
 * \remarks Use   DBCMDROW() macro instead.  
 * \sa DBCMDROW(), dbnextrow(), dbresults(), DBROWS(), DBROWTYPE().
 */
RETCODE
dbcmdrow(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;

	tdsdump_log(TDS_DBG_FUNC, "dbcmdrow(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	tds = dbproc->tds_socket;
	if (tds->res_info)
		return SUCCEED;
	return FAIL;
}

/**
 * \ingroup dblib_core
 * \brief Get column ID of a compute column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param column Nth column in \a computeid, starting from 1.
 * \return Nth column in the base result set, on which \a column was computed.  
 * \sa dbadata(), dbadlen(), dbaltlen(), dbgetrow(), dbnextrow(), dbnumalts(), dbprtype(). 
 */
int
dbaltcolid(DBPROCESS * dbproc, int computeid, int column)
{
	TDSCOLUMN *curcol;

	tdsdump_log(TDS_DBG_FUNC, "dbaltcolid(%p, %d, %d)\n", dbproc, computeid, column);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	curcol = dbacolptr(dbproc, computeid, column, 0);
	if (!curcol)
		return -1;

	return curcol->column_operand;
}

/**
 * \ingroup dblib_core
 * \brief Get size of data in a compute column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param column Nth column in \a computeid, starting from 1.
 * \return size of the data, in bytes.
 * \retval -1 no such \a column or \a computeid. 
 * \retval 0 data are NULL.
 * \sa dbadata(), dbaltlen(), dbalttype(), dbgetrow(), dbnextrow(), dbnumalts().
 */
DBINT
dbadlen(DBPROCESS * dbproc, int computeid, int column)
{
	TDSCOLUMN *colinfo;
	DBINT len;

	tdsdump_log(TDS_DBG_FUNC, "dbadlen(%p, %d, %d)\n", dbproc, computeid, column);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	colinfo = dbacolptr(dbproc, computeid, column, 0);
	if (!colinfo)
		return -1;

	len = colinfo->column_cur_size < 0? 0 : colinfo->column_cur_size;

	tdsdump_log(TDS_DBG_FUNC, "leaving dbadlen() type = %d, returning %d\n", colinfo->column_type, len);

	return len;
}

/**
 * \ingroup dblib_core
 * \brief Get datatype for a compute column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param column Nth column in \a computeid, starting from 1.
 * \return \c SYB* dataype token.
 * \retval -1 no such \a column or \a computeid. 
 * \sa dbadata(), dbadlen(), dbaltlen(), dbnextrow(), dbnumalts(), dbprtype().
 */
int
dbalttype(DBPROCESS * dbproc, int computeid, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbalttype(%p, %d, %d)\n", dbproc, computeid, column);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	colinfo = dbacolptr(dbproc, computeid, column, 0);
	if (!colinfo)
		return -1;

	switch (colinfo->column_type) {
	case SYBVARCHAR:
		return SYBCHAR;
	case SYBVARBINARY:
		return SYBBINARY;
	}
	return tds_get_conversion_type(colinfo->column_type, colinfo->column_size);
}

/**
 * \ingroup dblib_core
 * \brief Bind a compute column to a program variable.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param column Nth column in \a computeid, starting from 1.
 * \param vartype datatype of the host variable that will receive the data
 * \param varlen size of host variable pointed to \a varaddr
 * \param varaddr address of host variable
 * \retval SUCCEED everything worked.
 * \retval FAIL no such \a computeid or \a column, or no such conversion possible, or target buffer too small.
 * \sa dbadata(), dbaltbind_ps(), dbanullbind(), dbbind(), dbbind_ps(), dbconvert(),
 * 	dbconvert_ps(), dbnullbind(), dbsetnull(), dbsetversion(), dbwillconvert().
 */
RETCODE
dbaltbind(DBPROCESS * dbproc, int computeid, int column, int vartype, DBINT varlen, BYTE * varaddr)
{
	TDS_SERVER_TYPE srctype, desttype;
	TDSCOLUMN *colinfo = NULL;

	tdsdump_log(TDS_DBG_FUNC, "dbaltbind(%p, %d, %d, %d, %d, %p)\n", dbproc, computeid, column, vartype, varlen, varaddr);
	CHECK_PARAMETER(dbproc, SYBENULL, FAIL);

	colinfo = dbacolptr(dbproc, computeid, column, 1);
	if (!colinfo)
		return FAIL;
	CHECK_PARAMETER(varaddr, SYBEABNV, FAIL);

	dbproc->avail_flag = FALSE;

	srctype = tds_get_conversion_type(colinfo->column_type, colinfo->column_size);
	desttype = dblib_bound_type(vartype);
	if (desttype == TDS_INVALID_TYPE) {
		dbperror(dbproc, SYBEBTYP, 0);
		return FAIL;
	}

	if (!dbwillconvert(srctype, desttype)) {
		dbperror(dbproc, SYBEAAMT, 0);
		return FAIL;
	}

	colinfo->column_varaddr = (char *) varaddr;
	colinfo->column_bindtype = vartype;
	colinfo->column_bindlen = varlen;

	return SUCCEED;
}


/**
 * \ingroup dblib_core
 * \brief Get address of compute column data.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param column Nth column in \a computeid, starting from 1.
 * \return pointer to columns's data buffer.  
 * \retval NULL no such \a computeid or \a column.  
 * \sa dbadlen(), dbaltbind(), dbaltlen(), dbalttype(), dbgetrow(), dbnextrow(), dbnumalts().
 */
BYTE *
dbadata(DBPROCESS * dbproc, int computeid, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbadata(%p, %d, %d)\n", dbproc, computeid, column);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);

	colinfo = dbacolptr(dbproc, computeid, column, 0);
	if (!colinfo)
		return NULL;

	if (is_blob_col(colinfo)) {
		return (BYTE *) ((TDSBLOB *) colinfo->column_data)->textvalue;
	}

	return (BYTE *) colinfo->column_data;
}

/**
 * \ingroup dblib_core
 * \brief Get aggregation operator for a compute column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param column Nth column in \a computeid, starting from 1.
 * \return token value for the type of the compute column's aggregation operator.
 * \retval -1 no such \a computeid or \a column.  
 * \sa dbadata(), dbadlen(), dbaltlen(), dbnextrow(), dbnumalts(), dbprtype().
 */
int
dbaltop(DBPROCESS * dbproc, int computeid, int column)
{
	TDSCOLUMN *curcol;

	tdsdump_log(TDS_DBG_FUNC, "dbaltop(%p, %d, %d)\n", dbproc, computeid, column);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	if ((curcol=dbacolptr(dbproc, computeid, column, 0)) == NULL)
		return -1;

	return curcol->column_operator;
}

/**
 * \ingroup dblib_core
 * \brief Set db-lib or server option.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param option option to set.  
 * \param char_param value to set \a option to, if it wants a null-teminated ASCII string.  
 * \param int_param  value to set \a option to, if it wants an integer value.  
 * \retval SUCCEED everything worked.
 * \retval FAIL no such \a option, or insufficient memory, or unimplemented.  
 * \remarks Many are unimplemented.
 * \sa dbclropt(), dbisopt().
 * \todo Implement more options.  
 */
RETCODE
dbsetopt(DBPROCESS * dbproc, int option, const char *char_param, int int_param)
{
	char *cmd;
	RETCODE rc;
	int i;

	tdsdump_log(TDS_DBG_FUNC, "dbsetopt(%p, %d, %s, %d)\n", dbproc, option, char_param, int_param);
	CHECK_CONN(FAIL);
	CHECK_NULP(char_param, "dbsetopt", 3, FAIL);

	if ((option < 0) || (option >= DBNUMOPTIONS)) {
		dbperror(dbproc, SYBEUNOP, 0);
		return FAIL;
	}
	dbproc->dbopts[option].factive = 1;
	switch (option) {
	case DBARITHABORT:
	case DBARITHIGNORE:
	case DBCHAINXACTS:
	case DBFIPSFLAG:
	case DBISOLATION:
	case DBNOCOUNT:
	case DBNOEXEC:
	case DBPARSEONLY:
	case DBSHOWPLAN:
	case DBSTORPROCID:
	case DBQUOTEDIDENT:
		/* server options (on/off) */
		if (asprintf(&cmd, "set %s on\n", dbproc->dbopts[option].text) < 0) {
			return FAIL;
		}
		rc = dbstring_concat(&(dbproc->dboptcmd), cmd);
		free(cmd);
		return rc;
		break;
	case DBNATLANG:
	case DBDATEFIRST:
	case DBDATEFORMAT:
		/* server options (char_param) */
		if (asprintf(&cmd, "set %s %s\n", dbproc->dbopts[option].text, char_param) < 0) {
			return FAIL;
		}
		rc = dbstring_concat(&(dbproc->dboptcmd), cmd);
		free(cmd);
		return rc;
		break;
	case DBOFFSET:
		/* server option */
		/* requires param
		 * "select", "from", "table", "order", "compute",
		 * "statement", "procedure", "execute", or "param"
		 */
		break;
	case DBROWCOUNT:
		/* server option */
		/* requires param "0" to "2147483647" */
		break;
	case DBSTAT:
		/* server option */
		/* requires param "io" or "time" */
		break;
	case DBTEXTLIMIT:
		/* dblib option */
		/* requires param "0" to "2147483647" */
		/* dblib do not return more than this length from text/image */
		/* TODO required for PHP */
		break;
	case DBTEXTSIZE:
		/* server option */
		/* requires param "0" to "2147483647" */
		/* limit text/image from network */
		if (!char_param)
			char_param = "0";
		i = atoi(char_param);
		if (i < 0 || i > 2147483647)
			return FAIL;
		if (asprintf(&cmd, "set textsize %d\n", i) < 0)
			return FAIL;
		rc = dbstring_concat(&(dbproc->dboptcmd), cmd);
		free(cmd);
		return rc;

	case DBAUTH:
		/* ??? */
		break;
	case DBNOAUTOFREE:
		/* dblib option */
		break;
	case DBBUFFER:
		/* 
		 * Requires param "2" to "2147483647" 
		 * (0 or 1 is an error, < 0 yields the default 100) 
		 */
		{
			int nrows; 
			
			/* 100 is the default, according to Microsoft */
			if( !char_param )
				char_param = "100";

			nrows = atoi(char_param);

			nrows = (nrows < 0 )? 100 : nrows;

			if( 1 < nrows && nrows <= 2147483647 ) {
				buffer_set_capacity(dbproc, nrows);
				return SUCCEED;
			}
		}
		break;
	case DBPRCOLSEP:
	case DBPRLINELEN:
	case DBPRLINESEP:
		rc = dbstring_assign(&(dbproc->dbopts[option].param), char_param);
		return rc;
	case DBPRPAD:
		/*
		 * "If the character is not specified, the ASCII space character is used." 
		 * A NULL pointer to the pad character signifies that padding is turned off. 
		 */
		if (int_param) {
			rc = dbstring_assign(&(dbproc->dbopts[option].param), char_param? char_param : " ");
		} else {
			rc = dbstring_assign(&(dbproc->dbopts[option].param), NULL);
		}
		return rc;
		break;
	case DBSETTIME:
		if (char_param) {
			i = atoi(char_param);
			if (0 < i) {
				rc = dbstring_assign(&(dbproc->dbopts[option].param), char_param);
				if (rc == SUCCEED) {
					dbproc->tds_socket->query_timeout = i;
				}
				return rc;
			}
		}
		break;
	default:
		break;
	}
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbsetopt(option = %d)\n", option);
	return FAIL;
}

/**
 * \ingroup dblib_core
 * \brief Set interrupt handler for db-lib to use while blocked against a read from the server.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param chkintr
 * \param hndlintr
 * \sa dbcancel(), dbgetuserdata(), dbsetuserdata(), dbsetbusy(), dbsetidle().
 */
void
dbsetinterrupt(DBPROCESS * dbproc, DB_DBCHKINTR_FUNC chkintr, DB_DBHNDLINTR_FUNC hndlintr)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetinterrupt(%p, %p, %p)\n", dbproc, chkintr, hndlintr);
	CHECK_PARAMETER(dbproc, SYBENULL, );

	dbproc->chkintr = chkintr;
	dbproc->hndlintr = hndlintr;
}

/**
 * \ingroup dblib_rpc
 * \brief Determine if query generated a return status number.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval TRUE fetch return status with dbretstatus().  
 * \retval FALSE no return status.  
 * \sa dbnextrow(), dbresults(), dbretdata(), dbretstatus(), dbrpcinit(), dbrpcparam(), dbrpcsend().
 */
DBBOOL
dbhasretstat(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;

	tdsdump_log(TDS_DBG_FUNC, "dbhasretstat(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, FALSE);

	tds = dbproc->tds_socket;
	if (tds->has_status) {
		return TRUE;
	} else {
		return FALSE;
	}
}

/**
 * \ingroup dblib_rpc
 * \brief Fetch status value returned by query or remote procedure call.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return return value 
 * \sa dbhasretstat(), dbnextrow(), dbresults(), dbretdata(), dbrpcinit(), dbrpcparam(), dbrpcsend().
 */
DBINT
dbretstatus(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbretstatus(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);

	return dbproc->tds_socket->ret_status;
}

/**
 * \ingroup dblib_rpc
 * \brief Get count of output parameters filled by a stored procedure.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return How many, possibly zero.  
 * \remarks This name sounds funny.  
 * \sa 
 */
int
dbnumrets(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;
	TDS_INT result_type;

	tdsdump_log(TDS_DBG_FUNC, "dbnumrets(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);

	tds = dbproc->tds_socket;

	tdsdump_log(TDS_DBG_FUNC, "dbnumrets() finds %d columns\n", (tds->param_info? tds->param_info->num_cols : 0));

	/* try to fetch output parameters and return status, if we have not already done so */
	if (!tds->param_info) 
		tds_process_tokens(tds, &result_type, NULL, TDS_TOKEN_TRAILING);
		
	if (!tds->param_info)
		return 0;

	return tds->param_info->num_cols;
}

/**
 * \ingroup dblib_rpc
 * \brief Get name of an output parameter filled by a stored procedure.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param retnum Nth parameter between \c 1 and the return value from \c dbnumrets().
 * \returns ASCII null-terminated string, \c NULL if no such \a retnum.  
 * \sa dbnextrow(), dbnumrets(), dbresults(), dbretdata(), dbretlen(), dbrettype(), dbrpcinit(), dbrpcparam().
 */
char *
dbretname(DBPROCESS * dbproc, int retnum)
{
	TDSPARAMINFO *param_info;

	tdsdump_log(TDS_DBG_FUNC, "dbretname(%p, %d)\n", dbproc, retnum);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);

	if (!dbproc->tds_socket)
		return NULL;

	dbnumrets(dbproc);

	param_info = dbproc->tds_socket->param_info;
	if (!param_info || !param_info->columns || retnum < 1 || retnum > param_info->num_cols)
		return NULL;
	return tds_dstr_buf(&param_info->columns[retnum - 1]->column_name);
}

/**
 * \ingroup dblib_rpc
 * \brief Get value of an output parameter filled by a stored procedure.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param retnum Nth parameter between \c 1 and the return value from \c dbnumrets().
 * \returns Address of a return parameter value, or \c NULL if no such \a retnum.  
 * \sa dbnextrow(), dbnumrets(), dbresults(), dbretlen(), dbretname(), dbrettype(), dbrpcinit(), dbrpcparam().
 * \todo Handle blobs.
 */
BYTE *
dbretdata(DBPROCESS * dbproc, int retnum)
{
	TDSPARAMINFO *param_info;

	tdsdump_log(TDS_DBG_FUNC, "dbretdata(%p, %d)\n", dbproc, retnum);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);

	dbnumrets(dbproc);

	param_info = dbproc->tds_socket->param_info;
	if (!param_info || !param_info->columns || retnum < 1 || retnum > param_info->num_cols)
		return NULL;

	return _dbcoldata(param_info->columns[retnum - 1]);
}

/**
 * \ingroup dblib_rpc
 * \brief Get size of an output parameter filled by a stored procedure.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param retnum Nth parameter between \c 1 and the return value from \c dbnumrets().
 * \returns Size of a return parameter value, or \c NULL if no such \a retnum.  
 * \sa dbnextrow(), dbnumrets(), dbresults(), dbretdata(), dbretname(), dbrettype(), dbrpcinit(), dbrpcparam().
 */
int
dbretlen(DBPROCESS * dbproc, int retnum)
{
	TDSCOLUMN *column;
	TDSPARAMINFO *param_info;
	TDSSOCKET *tds;

	tdsdump_log(TDS_DBG_FUNC, "dbretlen(%p, %d)\n", dbproc, retnum);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	dbnumrets(dbproc);

	tds = dbproc->tds_socket;
	param_info = tds->param_info;
	if (!param_info || !param_info->columns || retnum < 1 || retnum > param_info->num_cols)
		return -1;

	column = param_info->columns[retnum - 1];
	if (column->column_cur_size < 0)
		return 0;

	return column->column_cur_size;
}

/**
 * \ingroup dblib_core
 * \brief Wait for results of a query from the server.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval SUCCEED everything worked, fetch results with \c dbnextresults().
 * \retval FAIL SQL syntax error, typically.  
 * \sa dbcmd(), dbfcmd(), DBIORDESC(), DBIOWDESC(), dbmoretext(), dbnextrow(),
	dbpoll(), DBRBUF(), dbresults(), dbretstatus(), dbrpcsend(), dbsettime(), dbsqlexec(),
	dbsqlsend(), dbwritetext().
 */
RETCODE
dbsqlok(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;
	TDS_INT result_type;
	RETCODE return_code = SUCCEED;
	prbuf_t prretbuf;

	tdsdump_log(TDS_DBG_FUNC, "dbsqlok(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	tds = dbproc->tds_socket;

	/*
	 * dbsqlok has been called after dbmoretext()
	 * This is the trigger to send the text data.
	 */

	if (dbproc->text_sent) {
		tds_flush_packet(tds);
		dbproc->text_sent = 0;
	}

	/* 
	 * See what the next packet from the server is.
	 * We want to skip any messages which are not processable. 
	 * We're looking for a result token or a done token.
         */
	for (;;) {
		TDSRET tds_code;
		int done_flags = 0;

		/* 
		 * If we hit an end token -- e.g. if the command
		 * submitted returned no data (like an insert) -- then
		 * we process the end token to extract the status code. 
		 */
		tdsdump_log(TDS_DBG_FUNC, "dbsqlok() not done, calling tds_process_tokens()\n");

		tds_code = tds_process_tokens(tds, &result_type, &done_flags, TDS_TOKEN_RESULTS);

		/* 
		 * The error flag may be set for any intervening DONEINPROC packet, in particular
		 * by a RAISERROR statement.  Microsoft db-lib returns FAIL in that case. 
		 */
		if (done_flags & TDS_DONE_ERROR) {
			return_code = FAIL;
		}
		
		switch (tds_code) {
		case TDS_NO_MORE_RESULTS:
			return SUCCEED;
			break;

		case TDS_SUCCESS:
			switch (result_type) {
			case TDS_ROWFMT_RESULT:
				buffer_free(&dbproc->row_buf);
				buffer_alloc(dbproc);
			case TDS_COMPUTEFMT_RESULT:
				dbproc->dbresults_state = _DB_RES_RESULTSET_EMPTY;
			case TDS_COMPUTE_RESULT:
			case TDS_ROW_RESULT:
				tdsdump_log(TDS_DBG_FUNC, "dbsqlok() found result token\n");
				return SUCCEED;
				break;
			case TDS_DONEINPROC_RESULT:
				break;
			case TDS_DONE_RESULT:
			case TDS_DONEPROC_RESULT:
				tdsdump_log(TDS_DBG_FUNC, "dbsqlok() end status is %s\n", prdbretcode(return_code, prretbuf));
#if 1
				if (done_flags & TDS_DONE_ERROR) {

					if (done_flags & TDS_DONE_MORE_RESULTS) {
						dbproc->dbresults_state = _DB_RES_NEXT_RESULT;
					} else {
						dbproc->dbresults_state = _DB_RES_NO_MORE_RESULTS;
					}

				} else {
					tdsdump_log(TDS_DBG_FUNC, "dbsqlok() end status was success\n");

					dbproc->dbresults_state = _DB_RES_SUCCEED;
				}

				return return_code;
				break;
#else
				int retcode = (done_flags & TDS_DONE_ERROR)? FAIL : SUCCEED;
				dbproc->dbresults_state = (done_flags & TDS_DONE_MORE_RESULTS)?
					_DB_RES_NEXT_RESULT : _DB_RES_NO_MORE_RESULTS;

				tdsdump_log(TDS_DBG_FUNC, "dbsqlok: returning %s with %s (%#x)\n", 
						prdbretcode(retcode), prdbresults_state(dbproc->dbresults_state), done_flags);
						
				if (retcode == SUCCEED && (done_flags & TDS_DONE_MORE_RESULTS))
					continue;
					 
				return retcode;
#endif
			default:
				tdsdump_log(TDS_DBG_FUNC, "%s %d: logic error: tds_process_tokens result_type %d\n", 
						__FILE__, __LINE__, result_type);
				break;
			}
			break;

		default:
			assert(TDS_FAILED(tds_code));
			return FAIL;
			break;
		}
	}

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Get count of columns in a compute row.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \return number of columns, else -1 if no such \a computeid.  
 * \sa dbadata(), dbadlen(), dbaltlen(), dbalttype(), dbgetrow(), dbnextrow(), dbnumcols(). 
 */
int
dbnumalts(DBPROCESS * dbproc, int computeid)
{
	TDSSOCKET *tds;
	TDSCOMPUTEINFO *info;
	TDS_SMALLINT compute_id;
	int i;

	tdsdump_log(TDS_DBG_FUNC, "dbnumalts(%p, %d)\n", dbproc, computeid);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	tds = dbproc->tds_socket;
	compute_id = computeid;

	for (i = 0;; ++i) {
		if (i >= tds->num_comp_info)
			return -1;
		info = tds->comp_info[i];
		if (info->computeid == compute_id)
			break;
	}

	return info->num_cols;
}

/**
 * \ingroup dblib_core
 * \brief Get count of \c COMPUTE clauses for a result set.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return number of compute clauses for the current query, possibly zero.  
 * \sa dbnumalts(), dbresults().
 */
int
dbnumcompute(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;

	tdsdump_log(TDS_DBG_FUNC, "dbnumcompute(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	tds = dbproc->tds_socket;

	return tds->num_comp_info;
}


/**
 * \ingroup dblib_core
 * \brief Get \c bylist for a compute row.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param size \em output: size of \c bylist buffer whose address is returned, possibly zero.  
 * \return address of \c bylist for \a computeid.  
 * \retval NULL no such \a computeid.
 * \remarks Do not free returned pointer.  
 * \sa dbadata(), dbadlen(), dbaltlen(), dbalttype(), dbcolname(), dbgetrow(), dbnextrow().
 */
BYTE *
dbbylist(DBPROCESS * dbproc, int computeid, int *size)
{
	TDSSOCKET *tds;
	TDSCOMPUTEINFO *info;
	int i;
	const TDS_SMALLINT byte_flag = -0x8000;

	tdsdump_log(TDS_DBG_FUNC, "dbbylist(%p, %d, %p)\n", dbproc, computeid, size);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);

	tds = dbproc->tds_socket;

	for (i = 0;; ++i) {
		if (i >= tds->num_comp_info) {
			if (size)
				*size = 0;
			return NULL;
		}
		info = tds->comp_info[i];
		if (info->computeid == computeid)
			break;
	}

	if (size)
		*size = info->by_cols;

	/*
	 * libtds stores this information using TDS_SMALLINT so we 
	 * have to convert it. We can do this because libtds just
	 * stores these data.
	 */
	if (info->by_cols > 0 && info->bycolumns[0] != byte_flag) {
		int n;
		TDS_TINYINT *p = (TDS_TINYINT*) malloc(sizeof(info->bycolumns[0]) + info->by_cols);
		if (!p) {
			dbperror(dbproc, SYBEMEM, errno);
			return NULL;
		}
		for (n = 0; n < info->by_cols; ++n)
			p[sizeof(info->bycolumns[0]) + n] = info->bycolumns[n] > 255 ? 255 : info->bycolumns[n];
		*((TDS_SMALLINT *)p) = byte_flag;
		free(info->bycolumns);
		info->bycolumns = (TDS_SMALLINT *) p;
	}
	return (BYTE *) (&info->bycolumns[1]);
}

/** \internal
 * \ingroup dblib_internal
 * \brief Check if \a dbproc is an ex-parrot.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval TRUE process has been marked \em dead.
 * \retval FALSE process is OK.  
 * \remarks dbdead() does not communicate with the server.  
 * 	Unless a previously db-lib marked \a dbproc \em dead, dbdead() returns \c FALSE.  
 * \sa dberrhandle().
 */
DBBOOL
dbdead(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbdead(%p) [%s]\n", dbproc, dbproc? IS_TDSDEAD(dbproc->tds_socket)? "dead":"alive" : "quite dead");

	if( NULL == dbproc ) 
		return TRUE;

	if (IS_TDSDEAD(dbproc->tds_socket))
		return TRUE;
	else
		return FALSE;
}

/** \internal
 * \ingroup dblib_internal
 * \brief default error handler for db-lib (handles library-generated errors)
 * 
 * The default error handler doesn't print anything.  If you want to see your messages printed, 
 * install an error handler.  If you think that should be an optional compile- or run-time default, 
 * submit a patch.  It could be done.  
 * 
 * \sa DBDEAD(), dberrhandle().
 */
/* Thus saith Sybase:
 *     "If the user does not supply an error handler (or passes a NULL pointer to 
 *	dberrhandle), DB-Library will exhibit its default error-handling 
 *	behavior: It will abort the program if the error has made the affected 
 *	DBPROCESS unusable (the user can call DBDEAD to determine whether 
 *	or not a DBPROCESS has become unusable). If the error has not made the 
 *	DBPROCESS unusable, DB-Library will simply return an error code to its caller." 
 *
 * It is not the error handler, however, that aborts anything.  It is db-lib, cf. dbperror().  
 */ 
static int
default_err_handler(DBPROCESS * dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr)
{
	tdsdump_log(TDS_DBG_FUNC, "default_err_handler %p, %d, %d, %d, %p, %p", dbproc, severity, dberr, oserr, dberrstr, oserrstr);

	if (DBDEAD(dbproc) && (!dbproc || !dbproc->msdblib)) {
		return INT_EXIT;
	}
	
	if (!dbproc || !dbproc->msdblib) {	/* i.e. Sybase behavior */
		switch(dberr) {
		case SYBETIME:
			return INT_EXIT;
		default: 
			break;
		}
	}
	return INT_CANCEL;
}

/**
 * \ingroup dblib_core
 * \brief Set an error handler, for messages from db-lib.
 * 
 * \param handler pointer to callback function that will handle errors.
 *        Pass NULL to restore the default handler.  
 * \return address of prior handler, or NULL if none was previously installed. 
 * \sa DBDEAD(), dbmsghandle().
 */
EHANDLEFUNC
dberrhandle(EHANDLEFUNC handler)
{
	EHANDLEFUNC old_handler = _dblib_err_handler;

	tdsdump_log(TDS_DBG_FUNC, "dberrhandle(%p)\n", handler);

	_dblib_err_handler = handler? handler : default_err_handler;
	
	return (old_handler == default_err_handler)? NULL : old_handler;
}

/**
 * \ingroup dblib_core
 * \brief Set a message handler, for messages from the server.
 * 
 * \param handler address of the function that will process the messages.
 * \sa DBDEAD(), dberrhandle().
 */
MHANDLEFUNC
dbmsghandle(MHANDLEFUNC handler)
{
	MHANDLEFUNC retFun = _dblib_msg_handler;

	tdsdump_log(TDS_DBG_FUNC, "dbmsghandle(%p)\n", handler);

	_dblib_msg_handler = handler;
	return retFun;
}

#if defined(DBLIB_UNIMPLEMENTED)
/**
 * \ingroup dblib_money
 * \brief Add two DBMONEY values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 first operand.
 * \param m2 other operand. 
 * \param sum \em output: result of computation.  
 * \retval SUCCEED Always.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmnyadd(DBPROCESS * dbproc, DBMONEY * m1, DBMONEY * m2, DBMONEY * sum)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnyadd(%p, %p, %p, %p)\n", dbproc, m1, m2, sum);
	CHECK_CONN(FAIL);
	CHECK_NULP(m1,  "dbmnyadd", 2, FAIL);
	CHECK_NULP(m2,  "dbmnyadd", 3, FAIL);
	CHECK_NULP(sum, "dbmnyadd", 4, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmnyadd()\n");
	return SUCCEED;
}


/**
 * \ingroup dblib_money
 * \brief Subtract two DBMONEY values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 first operand.
 * \param m2 other operand, subtracted from \a m1. 
 * \param difference \em output: result of computation.  
 * \retval SUCCEED Always.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmnysub(DBPROCESS * dbproc, DBMONEY * m1, DBMONEY * m2, DBMONEY * difference)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnysub(%p, %p, %p, %p)\n", dbproc, m1, m2, difference);
	CHECK_CONN(FAIL);
	CHECK_NULP(m1, "dbmnysub", 2, FAIL);
	CHECK_NULP(m2, "dbmnysub", 3, FAIL);
	CHECK_NULP(difference, "dbmnysub", 4, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmnysyb()\n");
	return SUCCEED;
}

/**
 * \ingroup dblib_money
 * \brief Multiply two DBMONEY values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 first operand.
 * \param m2 other operand. 
 * \param prod \em output: result of computation.  
 * \retval SUCCEED Always.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmnymul(DBPROCESS * dbproc, DBMONEY * m1, DBMONEY * m2, DBMONEY * prod)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnymul(%p, %p, %p, %p)\n", dbproc, m1, m2, prod);
	CHECK_CONN(FAIL);
	CHECK_NULP(m1, "dbmnymul", 2, FAIL);
	CHECK_NULP(m2, "dbmnymul", 3, FAIL);
	CHECK_NULP(prod, "dbmnymul", 4, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmnymul()\n");
	return SUCCEED;
}

/**
 * \ingroup dblib_money
 * \brief Divide two DBMONEY values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 dividend.
 * \param m2 divisor. 
 * \param quotient \em output: result of computation.  
 * \retval SUCCEED Always.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmnydivide(DBPROCESS * dbproc, DBMONEY * m1, DBMONEY * m2, DBMONEY * quotient)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnydivide(%p, %p, %p, %p)\n", dbproc, m1, m2, quotient);
	CHECK_CONN(FAIL);
	CHECK_NULP(m1, "dbmnydivide", 2, FAIL);
	CHECK_NULP(m2, "dbmnydivide", 3, FAIL);
	CHECK_NULP(quotient, "dbmnydivide", 4, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmnydivide()\n");
	return SUCCEED;
}
#endif

/**
 * \ingroup dblib_money
 * \brief Compare two DBMONEY values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 some money.
 * \param m2 some other money. 
 * \retval 0 m1 == m2. 
 * \retval -1 m1 < m2.
 * \retval  1 m1 > m2.
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
int
dbmnycmp(DBPROCESS * dbproc, DBMONEY * m1, DBMONEY * m2)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnycmp(%p, %p, %p)\n", dbproc, m1, m2);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);
	CHECK_NULP(m1, "dbmnycmp", 2, 0);
	CHECK_NULP(m2, "dbmnycmp", 3, 0);

	if (m1->mnyhigh < m2->mnyhigh) {
		return -1;
	}
	if (m1->mnyhigh > m2->mnyhigh) {
		return 1;
	}
	if (m1->mnylow < m2->mnylow) {
		return -1;
	}
	if (m1->mnylow > m2->mnylow) {
		return 1;
	}
	return 0;
}

#if defined(DBLIB_UNIMPLEMENTED)
/**
 * \ingroup dblib_money
 * \brief Multiply a DBMONEY value by a positive integer, and add an amount. 
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param amount starting amount of money, also holds output.
 * \param multiplier amount to multiply \a amount by. 
 * \param addend amount to add to \a amount, after multiplying by \a multiplier. 
 * \retval SUCCEED Always.  
 * \remarks This function is goofy.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmnyscale(DBPROCESS * dbproc, DBMONEY * amount, int multiplier, int addend)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnyscale(%p, %p, %d, %d)\n", dbproc, amount, multiplier, addend);
	CHECK_CONN(FAIL);
	CHECK_NULP(amount, "dbmnyscale", 2, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmnyscale()\n");
	return SUCCEED;
}
#endif

/**
 * \ingroup dblib_money
 * \brief Set a DBMONEY value to zero.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param dest address of a DBMONEY structure.  
 * \retval SUCCEED unless \a amount is NULL.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmnyzero(DBPROCESS * dbproc, DBMONEY * dest)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnyzero(%p, %p)\n", dbproc, dest);
	CHECK_CONN(FAIL);
	CHECK_NULP(dest, "dbmnyzero", 2, FAIL);

	dest->mnylow = 0;
	dest->mnyhigh = 0;
	return SUCCEED;
}

/**
 * \ingroup dblib_money
 * \brief Get maximum positive DBMONEY value supported.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param amount address of a DBMONEY structure.  
 * \retval SUCCEED Always.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmnymaxpos(DBPROCESS * dbproc, DBMONEY * amount)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnymaxpos(%p, %p)\n", dbproc, amount);
	CHECK_CONN(FAIL);
	CHECK_NULP(amount, "dbmnymaxpos", 2, FAIL);

	amount->mnylow = 0xFFFFFFFFlu;
	amount->mnyhigh = 0x7FFFFFFFl;
	return SUCCEED;
}

/**
 * \ingroup dblib_money
 * \brief Get maximum negative DBMONEY value supported.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param amount address of a DBMONEY structure.  
 * \retval SUCCEED Always.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmnymaxneg(DBPROCESS * dbproc, DBMONEY * amount)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnymaxneg(%p, %p)\n", dbproc, amount);
	CHECK_CONN(FAIL);
	CHECK_NULP(amount, "dbmnymaxneg", 2, FAIL);

	amount->mnylow = 0;
	amount->mnyhigh = -0x80000000l;
	return SUCCEED;
}

#if defined(DBLIB_UNIMPLEMENTED)
/**
 * \ingroup dblib_money
 * \brief Get the least significant digit of a DBMONEY value, represented as a character.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param mnyptr \em input the money amount, \em and \em output: \a mnyptr divided by 10.  
 * \param digit the character value (between '0' and '9') of the rightmost digit in \a mnyptr.  
 * \param zero \em output: \c TRUE if \a mnyptr is zero on output, else \c FALSE.  
 * \retval SUCCEED Always.  
 * \sa dbconvert(), dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \remarks Unimplemented and likely to remain so.  We'd be amused to learn anyone wants this function.  
 * \todo Unimplemented.
 */
RETCODE
dbmnyndigit(DBPROCESS * dbproc, DBMONEY * mnyptr, DBCHAR * digit, DBBOOL * zero)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnyndigit(%p, %p, %s, %p)\n", dbproc, mnyptr, digit, zero);
	CHECK_CONN(FAIL);
	CHECK_NULP(mnyptr, "dbmnyndigit", 2, FAIL);
	CHECK_NULP(digit, "dbmnyndigit", 3, FAIL);
	CHECK_NULP(zero, "dbmnyndigit", 4, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmnyndigit()\n");
	return SUCCEED;
}

/**
 * \ingroup dblib_money
 * \brief Prepare a DBMONEY value for use with dbmnyndigit().
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param amount address of a DBMONEY structure.  
 * \param trim number of digits to trim from \a amount.
 * \param negative \em output: \c TRUE if \a amount < 0.  
 * \retval SUCCEED Always.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmnyinit(DBPROCESS * dbproc, DBMONEY * amount, int trim, DBBOOL * negative)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnyinit(%p, %p, %d, %p)\n", dbproc, amount, trim, negative);
	CHECK_CONN(FAIL);
	CHECK_NULP(amount, "dbmnyinit", 2, FAIL);
	CHECK_NULP(negative, "dbmnyinit", 4, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmnyinit()\n");
	return SUCCEED;
}


/**
 * \ingroup dblib_money
 * \brief Divide a DBMONEY value by a positive integer.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param amount address of a DBMONEY structure.  
 * \param divisor of \a amount.
 * \param remainder \em output: modulo of integer division.
 * \retval SUCCEED Always.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmnydown(DBPROCESS * dbproc, DBMONEY * amount, int divisor, int *remainder)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnydown(%p, %p, %d, %p)\n", dbproc, amount, divisor, remainder);
	CHECK_CONN(FAIL);
	CHECK_NULP(amount, "dbmnydown", 2, FAIL);
	CHECK_NULP(remainder, "dbmnydown", 4, FAIL);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmnydown()\n");
	return SUCCEED;
}
#endif

/**
 * \ingroup dblib_money
 * \brief Add $0.0001 to a DBMONEY value.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param amount address of a DBMONEY structure.  
 * \retval SUCCEED or FAIL if overflow or amount NULL.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmnyinc(DBPROCESS * dbproc, DBMONEY * amount)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnyinc(%p, %p)\n", dbproc, amount);
	CHECK_CONN(FAIL);
	CHECK_NULP(amount, "dbmnyinc", 2, FAIL);

	if (amount->mnylow != 0xFFFFFFFFlu) {
		++amount->mnylow;
		return SUCCEED;
	}
	if (amount->mnyhigh == 0x7FFFFFFFl)
		return FAIL;
	amount->mnylow = 0;
	++amount->mnyhigh;
	return SUCCEED;
}


/**
 * \ingroup dblib_money
 * \brief Subtract $0.0001 from a DBMONEY value.
 *
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param amount address of a DBMONEY structure.  
 * \retval SUCCEED or FAIL if overflow or amount NULL.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmnydec(DBPROCESS * dbproc, DBMONEY * amount)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnydec(%p, %p)\n", dbproc, amount);
	CHECK_CONN(FAIL);
	CHECK_NULP(amount, "dbmnydec", 2, FAIL);

	if (amount->mnylow != 0) {
		--amount->mnylow;
		return SUCCEED;
	}
	if (amount->mnyhigh == -0x80000000l)
		return FAIL;
	amount->mnylow = 0xFFFFFFFFlu;
	--amount->mnyhigh;
	return SUCCEED;
}

/**
 * \ingroup dblib_money
 * \brief Negate a DBMONEY value.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param src address of a DBMONEY structure.  
 * \param dest \em output: result of negation. 
 * \retval SUCCEED or FAIL if overflow or src/dest NULL.
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmnyminus(DBPROCESS * dbproc, DBMONEY * src, DBMONEY * dest)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnyminus(%p, %p, %p)\n", dbproc, src, dest);
	CHECK_CONN(FAIL);
	CHECK_NULP(src, "dbmnyminus", 2, FAIL);
	CHECK_NULP(dest, "dbmnyminus", 3, FAIL);

	if (src->mnyhigh == -0x80000000l && src->mnylow == 0)
		return FAIL;
	dest->mnyhigh = -src->mnyhigh;
	dest->mnylow = (~src->mnylow) + 1u;
	return SUCCEED;
}


/**
 * \ingroup dblib_money
 * \brief Negate a DBMONEY4 value.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param src address of a DBMONEY4 structure.  
 * \param dest \em output: result of negation. 
 * \retval SUCCEED usually.  
 * \retval FAIL  on overflow.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmny4minus(DBPROCESS * dbproc, DBMONEY4 * src, DBMONEY4 * dest)
{
	DBMONEY4 zero;

	tdsdump_log(TDS_DBG_FUNC, "dbmny4minus(%p, %p, %p)\n", dbproc, src, dest);
	CHECK_CONN(FAIL);
	CHECK_NULP(src, "dbmny4minus", 2, FAIL);
	CHECK_NULP(dest, "dbmny4minus", 3, FAIL);

	dbmny4zero(dbproc, &zero);
	return (dbmny4sub(dbproc, &zero, src, dest));
}

/**
 * \ingroup dblib_money
 * \brief Zero a DBMONEY4 value.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param dest address of a DBMONEY structure.  
 * \retval SUCCEED usually.  
 * \retval FAIL  \a dest is NULL.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmny4zero(DBPROCESS * dbproc, DBMONEY4 * dest)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmny4zero(%p, %p)\n", dbproc, dest);
	CHECK_CONN(FAIL);
	CHECK_NULP(dest, "dbmny4zero", 2, FAIL);

	dest->mny4 = 0;
	return SUCCEED;
}

/**
 * \ingroup dblib_money
 * \brief Add two DBMONEY4 values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 first operand.
 * \param m2 other operand. 
 * \param sum \em output: result of computation.  
 * \retval SUCCEED usually.  
 * \retval FAIL  on overflow.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmny4add(DBPROCESS * dbproc, DBMONEY4 * m1, DBMONEY4 * m2, DBMONEY4 * sum)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmny4add(%p, %p, %p, %p)\n", dbproc, m1, m2, sum);
	CHECK_CONN(FAIL);
	CHECK_NULP(m1, "dbmny4add", 2, FAIL);
	CHECK_NULP(m2, "dbmny4add", 3, FAIL);
	CHECK_NULP(sum, "dbmny4add", 4, FAIL);

	sum->mny4 = m1->mny4 + m2->mny4;
	if (((m1->mny4 < 0) && (m2->mny4 < 0) && (sum->mny4 >= 0))
	    || ((m1->mny4 > 0) && (m2->mny4 > 0) && (sum->mny4 <= 0))) {
		/* overflow */
		sum->mny4 = 0;
		return FAIL;
	}
	return SUCCEED;
}

/**
 * \ingroup dblib_money
 * \brief Subtract two DBMONEY4 values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 first operand.
 * \param m2 other operand, subtracted from \a m1. 
 * \param diff \em output: result of computation.  
 * \retval SUCCEED usually.  
 * \retval FAIL  on overflow.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
RETCODE
dbmny4sub(DBPROCESS * dbproc, DBMONEY4 * m1, DBMONEY4 * m2, DBMONEY4 * diff)
{

	tdsdump_log(TDS_DBG_FUNC, "dbmny4sub(%p, %p, %p, %p)\n", dbproc, m1, m2, diff);
	CHECK_CONN(FAIL);
	CHECK_NULP(m1, "dbmny4sub", 2, FAIL);
	CHECK_NULP(m2, "dbmny4sub", 3, FAIL);
	CHECK_NULP(diff, "dbmny4sub", 4, FAIL);

	diff->mny4 = m1->mny4 - m2->mny4;
	if (((m1->mny4 <= 0) && (m2->mny4 > 0) && (diff->mny4 > 0))
	    || ((m1->mny4 >= 0) && (m2->mny4 < 0) && (diff->mny4 < 0))) {
		/* overflow */
		diff->mny4 = 0;
		return FAIL;
	}
	return SUCCEED;
}

#if defined(DBLIB_UNIMPLEMENTED)
/**
 * \ingroup dblib_money
 * \brief Multiply two DBMONEY4 values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 first operand.
 * \param m2 other operand. 
 * \param prod \em output: result of computation.  
 * \retval SUCCEED usually.  
 * \retval FAIL a parameter is NULL.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmny4mul(DBPROCESS * dbproc, DBMONEY4 * m1, DBMONEY4 * m2, DBMONEY4 * prod)
{

	tdsdump_log(TDS_DBG_FUNC, "dbmny4mul(%p, %p, %p, %p)\n", dbproc, m1, m2, prod);
	CHECK_CONN(FAIL);
	CHECK_NULP(m1, "dbmny4mul", 2, FAIL);
	CHECK_NULP(m2, "dbmny4mul", 3, FAIL);
	CHECK_NULP(prod, "dbmny4mul", 4, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmny4mul()\n");
	return FAIL;
}

/**
 * \ingroup dblib_money
 * \brief Divide two DBMONEY4 values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 dividend.
 * \param m2 divisor. 
 * \param quotient \em output: result of computation.  
 * \retval SUCCEED usually.  
 * \retval FAIL a parameter is NULL.  
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 * \todo Unimplemented.
 */
RETCODE
dbmny4divide(DBPROCESS * dbproc, DBMONEY4 * m1, DBMONEY4 * m2, DBMONEY4 * quotient)
{

	tdsdump_log(TDS_DBG_FUNC, "dbmny4divide(%p, %p, %p, %p)\n", dbproc, m1, m2, quotient);
	CHECK_CONN(FAIL);
	CHECK_NULP(m1, "dbmny4divide", 2, FAIL);
	CHECK_NULP(m2, "dbmny4divide", 3, FAIL);
	CHECK_NULP(quotient, "dbmny4divide", 4, FAIL);

	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbmny4divide()\n");
	return FAIL;
}
#endif

/**
 * \ingroup dblib_money
 * \brief Compare two DBMONEY4 values.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param m1 some money.
 * \param m2 some other money. 
 * \retval 0 m1 == m2. 
 * \retval -1 m1 < m2.
 * \retval  1 m1 > m2.
 * \sa dbmnyadd(), dbmnysub(), dbmnymul(), dbmnydivide(), dbmnyminus(), dbmny4add(), dbmny4sub(), dbmny4mul(), dbmny4divide(), dbmny4minus().
 */
int
dbmny4cmp(DBPROCESS * dbproc, DBMONEY4 * m1, DBMONEY4 * m2)
{

	tdsdump_log(TDS_DBG_FUNC, "dbmny4cmp(%p, %p, %p)\n", dbproc, m1, m2);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);
	CHECK_NULP(m1, "dbmny4cmp", 2, 0);
	CHECK_NULP(m2, "dbmny4cmp", 3, 0);

	if (m1->mny4 < m2->mny4) {
		return -1;
	}
	if (m1->mny4 > m2->mny4) {
		return 1;
	}
	return 0;
}

/**
 * \ingroup dblib_money
 * \brief Copy a DBMONEY4 value.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param src address of a DBMONEY4 structure.  
 * \param dest \em output: new money. 
 * \retval SUCCEED or FAIL if src/dest NULL.
 * \sa dbmnycopy(), dbmnyminus(), dbmny4minus(). 
 */
RETCODE
dbmny4copy(DBPROCESS * dbproc, DBMONEY4 * src, DBMONEY4 * dest)
{

	tdsdump_log(TDS_DBG_FUNC, "dbmny4copy(%p, %p, %p)\n", dbproc, src, dest);
	CHECK_CONN(FAIL);
	CHECK_NULP(src, "dbmny4copy", 2, FAIL);
	CHECK_NULP(dest, "dbmny4copy", 3, FAIL);

	dest->mny4 = src->mny4;
	return SUCCEED;
}

/**
 * \ingroup dblib_datetime
 * \brief Compare DBDATETIME values, similar to strcmp(3).
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param d1 a \c DBDATETIME structure address
 * \param d2 another \c DBDATETIME structure address
 * \retval   0 d1 = d2.
 * \retval  -1 d1 < d2.
 * \retval   1 d1 > d2.
 * \sa dbdate4cmp(), dbmnycmp(), dbmny4cmp().
 */
int
dbdatecmp(DBPROCESS * dbproc, DBDATETIME * d1, DBDATETIME * d2)
{
	tdsdump_log(TDS_DBG_FUNC, "dbdatecmp(%p, %p, %p)\n", dbproc, d1, d2);
	CHECK_CONN(FAIL);
	CHECK_NULP(d1, "dbdatecmp", 2, 0);
	CHECK_NULP(d2, "dbdatecmp", 3, 0);

	if (d1->dtdays == d2->dtdays) {
		if (d1->dttime == d2->dttime)
			return 0;
		return d1->dttime > d2->dttime ? 1 : -1;
	}

	/* date 1 is before 1900 */
	if (d1->dtdays > 2958463) {

		if (d2->dtdays > 2958463)	/* date 2 is before 1900 */
			return d1->dtdays > d2->dtdays ? 1 : -1;
		return -1;
	}

	/* date 1 is after 1900 */
	if (d2->dtdays < 2958463)	/* date 2 is after 1900 */
		return d1->dtdays > d2->dtdays ? 1 : -1;
	return 1;
}

static RETCODE
dblib_datecrack(DBPROCESS * dbproc, BOOL nano_precision, DBDATEREC * output, int type, const void * data)
{
	TDSDATEREC dr;
	struct tds_sybase_dbdaterec *di = (struct tds_sybase_dbdaterec*) output;

	tdsdump_log(TDS_DBG_FUNC, "dblib_datecrack(%p, %d, %p, %d, %p)\n", dbproc, nano_precision, output, type, data);
	CHECK_NULP(output, "dbdatecrack", 2, FAIL);
	CHECK_PARAMETER(data, SYBENDTP, FAIL);

	if (TDS_FAILED(tds_datecrack(type, data, &dr)))
		return FAIL;

	di->dateyear = dr.year;
	di->quarter = dr.quarter;
	di->datemonth = dr.month;
	di->datedmonth = dr.day;
	di->datedyear = dr.dayofyear;
	di->datedweek = dr.weekday;
	di->datehour = dr.hour;
	di->dateminute = dr.minute;
	di->datesecond = dr.second;
	di->datetzone = dr.timezone;
	if (nano_precision)
		/* here we are writing to nanosecond field */
		di->datemsecond = dr.decimicrosecond * 100u;
	else
		di->datemsecond = dr.decimicrosecond / 10000u;
	/* Revert to compiled-in default if dbproc can't be used to find the runtime override. */
	if (dbproc ? dbproc->msdblib : dblib_msdblib) {
		++di->quarter;
		++di->datemonth;
		++di->datedweek;
	}
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Break a DBDATETIME value into useful pieces.
 *
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param di \em output: structure to contain the exploded parts of \a datetime.
 * \param datetime \em input: \c DBDATETIME to be converted.
 * \retval SUCCEED always.
 * \remarks The members of \a di have different names, depending on whether \c --with-msdblib was configured.
 *
 * If DBPROCESS is NULL, dbdatecrack() uses the compiled in default
 * value of MSDBLIB as of when libsybdb was compiled, irrespective of its value when the
 * application is compiled.  This can lead to incorrect results because Sybase and Microsoft use different
 * ranges -- [0,11] vs. [1,12] -- for the month.
 *
 * \sa dbconvert(), dbdata(), dbdatechar(), dbdatename(), dbdatepart(), tdsdbopen().
 */
RETCODE
dbdatecrack(DBPROCESS * dbproc, DBDATEREC * di, DBDATETIME * datetime)
{
	return dblib_datecrack(dbproc, FALSE, di, SYBDATETIME, datetime);
}

/**
 * \ingroup dblib_core
 * \brief Break any kind of date or time value into useful pieces.
 *
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param di \em output: structure to contain the exploded parts of \a datetime.
 * \param type \em input: \c type of date/time value returned by dbcoltype().
 * \param data \em input: \c date/time value to be converted.
 * \retval SUCCEED always.
 * \remarks The members of \a di have different names, depending on whether \c --with-msdblib was configured.
 *
 * This is an extension to dbdatecrack(), see it for more information.
 *
 * \sa dbdatecrack(), dbconvert(), dbdata(), dbdatechar(), dbdatename(), dbdatepart(), tdsdbopen().
 */
RETCODE
dbanydatecrack(DBPROCESS * dbproc, DBDATEREC2 * di, int type, const void *data)
{
	return dblib_datecrack(dbproc, TRUE, (DBDATEREC *) di, type, data);
}

#if defined(DBLIB_UNIMPLEMENTED)
/**
 * \ingroup dblib_core
 * \brief Clear remote passwords from the LOGINREC structure.
 * 
 * \param login structure to pass to dbopen().
 * \sa dblogin(), dbopen(), dbrpwset(), DBSETLAPP(), DBSETLHOST(), DBSETLPWD(), DBSETLUSER().  
 * \remarks Useful for remote stored procedure calls, but not in high demand from FreeTDS.  
 * \todo Unimplemented.
 */
void
dbrpwclr(LOGINREC * login)
{
	tdsdump_log(TDS_DBG_FUNC, "dbrpwclr(%p)\n", login);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbrpwclr()\n");
}

/**
 * \ingroup dblib_core
 * \brief Add a remote password to the LOGINREC structure.
 * 
 * \param login structure to pass to dbopen().
 * \param srvname server for which \a password should be used.  
 * \param password you guessed it, let's hope no else does.  
 * \param pwlen count of \a password, in bytes.
 * \remarks Useful for remote stored procedure calls, but not in high demand from FreeTDS.  
 * \sa dblogin(), dbopen(), dbrpwclr(), DBSETLAPP(), DBSETLHOST(), DBSETLPWD(), DBSETLUSER().  
 * \todo Unimplemented.
 */
RETCODE
dbrpwset(LOGINREC * login, char *srvname, char *password, int pwlen)
{
	tdsdump_log(TDS_DBG_FUNC, "dbrpwset(%p, %s, %s, %d)\n", login, srvname, password, pwlen);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbrpwset()\n");
	return SUCCEED;
}
#endif

/**
 * \ingroup dblib_core
 * \brief Get server process ID for a \c DBPROCESS.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return \em "spid", the server's process ID.  
 * \sa dbopen().  
 */
int
dbspid(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbspid(%p)\n", dbproc);
	CHECK_CONN(-1);

	return dbproc->tds_socket->conn->spid;
}

/**
 * \ingroup dblib_core
 * \brief Associate client-allocated (and defined) data with a \c DBPROCESS.   
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param ptr address of client-defined data.
 * \remarks \a ptr is the location of user data that \c db-lib will associate with \a dbproc. 
 * The client allocates the buffer addressed by \a ptr.  \c db-lib never examines or uses the information; 
 * it just stashes the pointer for later retrieval by the application with \c dbgetuserdata().  
 * \sa dbgetuserdata().  
 */
void
dbsetuserdata(DBPROCESS * dbproc, BYTE * ptr)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetuserdata(%p, %p)\n", dbproc, ptr);
	CHECK_PARAMETER(dbproc, SYBENULL, );

	dbproc->user_data = ptr;
}

/**
 * \ingroup dblib_core
 * \brief Get address of user-allocated data from a \c DBPROCESS.   
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return address of user-defined data that \c db-lib associated with \a dbproc when the client called  dbsetuserdata(). 
 * \retval undefined (probably \c NULL) dbsetuserdata() was not previously called.  
 * \sa dbsetuserdata().  
 */
BYTE *
dbgetuserdata(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbgetuserdata(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);

	return dbproc->user_data;
}

/**
 * \ingroup dblib_core
 * \brief Specify a db-lib version level.
 * 
 * \param version anything, really. 
 * \retval SUCCEED Always.  
 * \remarks No effect on behavior of \c db-lib in \c FreeTDS.  
 * \sa 
 */
RETCODE
dbsetversion(DBINT version)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetversion(%d)\n", version);

	switch (version ) {
	case DBVERSION_42:
	case DBVERSION_46:
	case DBVERSION_100:
	case DBVERSION_70:
	case DBVERSION_71:
	case DBVERSION_72:
	case DBVERSION_73:
		g_dblib_version = version;
		return SUCCEED;
	default:
		break;
	}
	
	dbperror(NULL, SYBEIVERS, 0);
	return FAIL;
}

/**
 * \ingroup dblib_money
 * \brief Copy a DBMONEY value.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param src address of a DBMONEY structure.  
 * \param dest \em output: new money. 
 * \retval SUCCEED always, unless \a src or \a dest is \c NULL.  
 * \sa 
 */
RETCODE
dbmnycopy(DBPROCESS * dbproc, DBMONEY * src, DBMONEY * dest)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmnycopy(%p, %p, %p)\n", dbproc, src, dest);
	CHECK_CONN(FAIL);
	CHECK_NULP(src, "dbmnycopy", 2, FAIL);
	CHECK_NULP(dest, "dbmnycopy", 3, FAIL);

	dest->mnylow = src->mnylow;
	dest->mnyhigh = src->mnyhigh;
	return SUCCEED;
}


/**
 * \ingroup dblib_core
 * \brief Cancel the query currently being retrieved, discarding all pending rows.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa 
 */
RETCODE
dbcanquery(DBPROCESS * dbproc)
{
	TDSRET rc;
	TDS_INT result_type;

	tdsdump_log(TDS_DBG_FUNC, "dbcanquery(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	if (IS_TDSDEAD(dbproc->tds_socket))
		return FAIL;

	/* Just throw away all pending rows from the last query */

	rc = tds_process_tokens(dbproc->tds_socket, &result_type, NULL, TDS_STOPAT_ROWFMT|TDS_RETURN_DONE);

	if (TDS_FAILED(rc))
		return FAIL;

	dbproc->dbresults_state = _DB_RES_NEXT_RESULT;

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Erase the command buffer, in case \c DBNOAUTOFREE was set with dbsetopt(). 
 * 
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa dbcmd(), dbfcmd(), dbgetchar(), dbsqlexec(), dbsqlsend(), dbsetopt(), dbstrcpy(), dbstrlen().  
 */
void
dbfreebuf(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbfreebuf(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, );

	if (dbproc->dbbuf)
		TDS_ZERO_FREE(dbproc->dbbuf);
	dbproc->dbbufsz = 0;
}

/**
 * \ingroup dblib_core
 * \brief Reset an option.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param option to be turned off.
 * \param param clearing some options requires a parameter, believe it or not.  
 * \retval SUCCEED \a option and \a parameter seem sane.
 * \retval FAIL no such \a option.
 * \remarks Only the following options are recognized:
	- DBARITHABORT
	- DBARITHIGNORE
	- DBCHAINXACTS
	- DBFIPSFLAG
	- DBISOLATION
	- DBNOCOUNT
	- DBNOEXEC
	- DBPARSEONLY
	- DBSHOWPLAN
	- DBSTORPROCID
	- DBQUOTEDIDENT
	- DBSETTIME
 * \sa dbisopt(), dbsetopt().
 */
RETCODE
dbclropt(DBPROCESS * dbproc, int option, const char param[])
{
	char *cmd;

	tdsdump_log(TDS_DBG_FUNC, "dbclropt(%p, %d, %s)\n", dbproc, option, param);
	CHECK_CONN(FAIL);
	if (option != DBSETTIME) {
		CHECK_NULP(param, "dbclropt", 3, FAIL);
	}

	if ((option < 0) || (option >= DBNUMOPTIONS)) {
		return FAIL;
	}
	dbproc->dbopts[option].factive = 0;
	switch (option) {
	case DBARITHABORT:
	case DBARITHIGNORE:
	case DBCHAINXACTS:
	case DBFIPSFLAG:
	case DBISOLATION:
	case DBNOCOUNT:
	case DBNOEXEC:
	case DBPARSEONLY:
	case DBSHOWPLAN:
	case DBSTORPROCID:
	case DBQUOTEDIDENT:
		/* server options (on/off) */
		if (asprintf(&cmd, "set %s off\n", dbproc->dbopts[option].text) < 0) {
			return FAIL;
		}
		dbstring_concat(&(dbproc->dboptcmd), cmd);
		free(cmd);
		break;
	case DBBUFFER:
		buffer_set_capacity(dbproc, 1); /* frees row_buf->rows */
		return SUCCEED;
		break;
	case DBSETTIME:
		tds_mutex_lock(&dblib_mutex);
		/*
		 * Use global value of query timeout set by dbsettime() if any,
		 * otherwise set it to zero just like tds_init_socket() does.
		 */
		if (g_dblib_ctx.query_timeout > 0) {
			dbproc->tds_socket->query_timeout = g_dblib_ctx.query_timeout;
		} else {
			dbproc->tds_socket->query_timeout = 0;
		}
		tds_mutex_unlock(&dblib_mutex);
		return SUCCEED;
		break;
	default:
		break;
	}
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbclropt(option = %d)\n", option);
	return FAIL;
}

/**
 * \ingroup dblib_core
 * \brief Get value of an option
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param option the option
 * \param param a parameter to \a option. 
 * \sa dbclropt(), dbsetopt().
 */
DBBOOL
dbisopt(DBPROCESS * dbproc, int option, const char param[])
{
	tdsdump_log(TDS_DBG_FUNC, "dbisopt(%p, %d, %s)\n", dbproc, option, param);
	CHECK_PARAMETER(dbproc, SYBENULL, FALSE);
	/* sometimes param can be NULL */
	
	if ((option < 0) || (option >= DBNUMOPTIONS)) {
		return FALSE;
	}
	return dbproc->dbopts[option].factive;
}

/** \internal
 * \ingroup dblib_internal
 * \brief Get number of the row currently being read.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return ostensibly the row number, or 0 if no rows have been read yet.  
 * \retval 0 Always.  
 * \sa DBCURROW(), dbclrbuf(), DBFIRSTROW(), dbgetrow(), DBLASTROW(), dbnextrow(), dbsetopt(),.  
 * \todo Unimplemented.
 */
DBINT
dbcurrow(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbcurrow(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbcurrow()\n");
	return 0;
}


/** \internal
 * \ingroup dblib_internal
 * \brief Get returned row's type.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa DBROWTYPE().  
 */
STATUS
dbrowtype(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbrowtype(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, NO_MORE_ROWS);
	return dbproc->row_type;
}


/** \internal
 * \ingroup dblib_internal
 * \brief Get number of the row just returned.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa DBCURROW().
 * \todo Unimplemented.
 */
int
dbcurcmd(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbcurcmd(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbcurcmd()\n");
	return 0;
}


/** 
 * \ingroup dblib_core
 * \brief See if more commands are to be processed.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa DBMORECMDS(). DBCMDROW(), dbresults(), DBROWS(), DBROWTYPE().  
 */
RETCODE
dbmorecmds(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbmorecmds(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	if (dbproc->tds_socket->res_info == NULL) {
		return FAIL;
	}

	if (dbproc->tds_socket->res_info->more_results == 0) {
		tdsdump_log(TDS_DBG_FUNC, "more_results == 0; returns FAIL\n");
		return FAIL;
	}
	
	assert(dbproc->tds_socket->res_info->more_results == 1);
	
	tdsdump_log(TDS_DBG_FUNC, "more_results == 1; returns SUCCEED\n");
	
	return SUCCEED;
}

/**
 * \ingroup dblib_rpc
 * \brief Get datatype of a stored procedure's return parameter.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param retnum Nth return parameter, between 1 and \c dbnumrets().  
 * \return SYB* datatype token, or -1 if \a retnum is out of range. 
 * \sa dbnextrow(), dbnumrets(), dbprtype(), dbresults(), dbretdata(), dbretlen(), dbretname(), dbrpcinit(), dbrpcparam(). 
 */
int
dbrettype(DBPROCESS * dbproc, int retnum)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbrettype(%p, %d)\n", dbproc, retnum);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);
	assert(dbproc->tds_socket);
	assert(dbproc->tds_socket->param_info);

	if (retnum < 1 || retnum > dbproc->tds_socket->param_info->num_cols)
		return -1;

	colinfo = dbproc->tds_socket->param_info->columns[retnum - 1];

	return tds_get_conversion_type(colinfo->column_type, colinfo->column_size);
}

/**
 * \ingroup dblib_core
 * \brief Get size of the command buffer, in bytes. 
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa dbcmd(), dbfcmd(), dbfreebuf(), dbgetchar(), dbstrcpy(). 
 */
int
dbstrlen(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbstrlen(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);
	
	return dbproc->dbbufsz;
}


/**
 * \ingroup dblib_core
 * \brief Get address of a position in the command buffer.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param pos offset within the command buffer, starting at \em 0.   
 * \remarks A bit overspecialized, this one.  
 * \sa dbcmd(), dbfcmd(), dbfreebuf(), dbstrcpy(), dbstrlen(),
 */
char *
dbgetchar(DBPROCESS * dbproc, int pos)
{
	tdsdump_log(TDS_DBG_FUNC, "dbgetchar(%p, %d)\n", dbproc, pos);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);
	tdsdump_log(TDS_DBG_FUNC, "dbgetchar() bufsz = %d, pos = %d\n", dbproc->dbbufsz, pos);

	if (dbproc->dbbufsz > 0) {
		if (pos >= 0 && pos < (dbproc->dbbufsz - 1))
			return (char *) &dbproc->dbbuf[pos];
		else
			return NULL;
	} else
		return NULL;
}

/**
 * \ingroup dblib_core
 * \brief Get a copy of a chunk of the command buffer.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param start position in the command buffer to start copying from, starting from \em 0.  
 *  	If start is past the end of the command buffer, dbstrcpy() inserts a null terminator at dest[0].
 * \param numbytes number of bytes to copy. 
 	- If -1, dbstrcpy() copies the whole command buffer.  
	- If  0 dbstrcpy() writes a \c NULL to dest[0]. 
	- If the command buffer contains fewer than \a numbytes (taking \a start into account) dbstrcpy() 
	copies the rest of it.  
 * \param dest \em output: the buffer to write to.  Make sure it's big enough.  
 * \retval SUCCEED the inputs were valid and \a dest was affected.  
 * \retval FAIL \a start < 0 or \a numbytes < -1.  
 * \sa dbcmd(), dbfcmd(), dbfreebuf(), dbgetchar(), dbstrlen().  
 */
RETCODE
dbstrcpy(DBPROCESS * dbproc, int start, int numbytes, char *dest)
{
	tdsdump_log(TDS_DBG_FUNC, "dbstrcpy(%p, %d, %d, %s)\n", dbproc, start, numbytes, dest);
	CHECK_CONN(FAIL);
	CHECK_NULP(dest, "dbstrcpy", 4, FAIL);

	if (start < 0) {
		dbperror(dbproc, SYBENSIP, 0);
		return FAIL;
	}
	if (numbytes < -1) {
		dbperror(dbproc, SYBEBNUM, 0);
		return FAIL;
	}
	dest[0] = 0;		/* start with empty string being returned */
	if (dbproc->dbbufsz > 0 && start < dbproc->dbbufsz) {
		if (numbytes == -1)
			numbytes = dbproc->dbbufsz - start;
		if (start + numbytes > dbproc->dbbufsz)
			numbytes = dbproc->dbbufsz - start;
		memcpy(dest, (char *) &dbproc->dbbuf[start], numbytes);
		dest[numbytes] = '\0';
	}
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief safely quotes character values in SQL text.  
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param src input string.
 * \param srclen length of \a src in bytes, or -1 to indicate it's null-terminated.  
 * \param dest \em output: client-provided output buffer. 
 * \param destlen size of \a dest in bytes, or -1 to indicate it's "big enough" and the data should be null-terminated.  
 * \param quotetype
	- \c DBSINGLE Doubles all single quotes (').
	- \c DBDOUBLE Doubles all double quotes (").
	- \c DBBOTH   Doubles all single and double quotes.
 * \retval SUCCEED everything worked.
 * \retval FAIL no such \a quotetype, or insufficient room in \a dest.
 * \sa dbcmd(), dbfcmd(). 
 */
RETCODE
dbsafestr(DBPROCESS * dbproc, const char *src, DBINT srclen, char *dest, DBINT destlen, int quotetype)
{
	int i, j = 0;
	int squote = FALSE, dquote = FALSE;

	tdsdump_log(TDS_DBG_FUNC, "dbsafestr(%p, %s, %d, %s, %d, %d)\n", dbproc, src, srclen, dest, destlen, quotetype);
	CHECK_NULP(src, "dbsafestr", 2, FAIL);
	CHECK_NULP(dest, "dbsafestr", 4, FAIL);

	/* check parameters */
	if (srclen < -1 || destlen < -1)
		return FAIL;

	if (srclen == -1)
		srclen = (int)strlen(src);

	if (quotetype == DBSINGLE || quotetype == DBBOTH)
		squote = TRUE;
	if (quotetype == DBDOUBLE || quotetype == DBBOTH)
		dquote = TRUE;

	/* return FAIL if invalid quotetype */
	if (!dquote && !squote)
		return FAIL;


	for (i = 0; i < srclen; i++) {

		/* dbsafestr returns fail if the deststr is not big enough */
		/* need one char + one for terminator */
		if (destlen >= 0 && j >= destlen)
			return FAIL;

		if (squote && src[i] == '\'')
			dest[j++] = '\'';
		else if (dquote && src[i] == '\"')
			dest[j++] = '\"';

		if (destlen >= 0 && j >= destlen)
			return FAIL;

		dest[j++] = src[i];
	}

	if (destlen >= 0 && j >= destlen)
		return FAIL;

	dest[j] = '\0';
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Print a token value's name to a buffer
 * 
 * \param token server SYB* value, e.g. SYBINT.  

 * \return ASCII null-terminated string.  
 * \sa dbaltop(), dbalttype(), dbcoltype(), dbrettype().
 */
const char *
dbprtype(int token)
{
	tdsdump_log(TDS_DBG_FUNC, "dbprtype(%d)\n", token);
	return tds_prtype(token);
}

/**
 * \ingroup dblib_core
 * \brief describe table column attributes with a single call (Freetds-only API function modelled on dbcolinfo)
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column Nth in the result set, starting from 1.
 * \param pdbcol address of structure to be populated by this function.  
 * \return SUCCEED or FAIL. 
 * \sa dbcolinfo().
 */
RETCODE
dbtablecolinfo (DBPROCESS *dbproc, DBINT column, DBCOL *pdbcol )
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbtablecolinfo(%p, %d, %p)\n", dbproc, column, pdbcol);
	CHECK_CONN(FAIL);
	CHECK_NULP(pdbcol, "dbtablecolinfo", 3, FAIL);
	DBPERROR_RETURN(pdbcol->SizeOfStruct != sizeof(DBCOL)
			&& pdbcol->SizeOfStruct != sizeof(DBCOL2), SYBECOLSIZE);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo)
		return FAIL;

	strlcpy(pdbcol->Name, tds_dstr_cstr(&colinfo->column_name), sizeof(pdbcol->Name));
	strlcpy(pdbcol->ActualName, tds_dstr_cstr(&colinfo->column_name), sizeof(pdbcol->ActualName));

	pdbcol->Type = tds_get_conversion_type(colinfo->column_type, colinfo->column_size);
	pdbcol->UserType = colinfo->column_usertype;
	pdbcol->MaxLength = colinfo->column_size;
	if (colinfo->column_nullable)
		pdbcol->Null = TRUE;
	else
		pdbcol->Null = FALSE;

	pdbcol->VarLength = FALSE;

	if (colinfo->column_nullable
	    || is_nullable_type(colinfo->column_type))
		pdbcol->VarLength = TRUE;
	
	pdbcol->Precision = colinfo->column_prec;
	pdbcol->Scale = colinfo->column_scale;

	pdbcol->Updatable = colinfo->column_writeable ? TRUE : FALSE;
	pdbcol->Identity = colinfo->column_identity ? TRUE : FALSE;

	if (pdbcol->SizeOfStruct >= sizeof(DBCOL2)) {
		DBCOL2 *col = (DBCOL2 *) pdbcol;
		TDSRET rc;

		col->ServerType = colinfo->on_server.column_type;
		col->ServerMaxLength = colinfo->on_server.column_size;

		rc = tds_get_column_declaration(dbproc->tds_socket, colinfo, col->ServerTypeDeclaration);
		if (TDS_FAILED(rc))
			return FAIL;
	}

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Get text timestamp for a column in the current row.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column number of the column in the \c SELECT statement, starting at 1.
 * \return timestamp for \a column, may be NULL.
 * \sa dbtxptr(), dbwritetext().  
 */
DBBINARY *
dbtxtimestamp(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;
	TDSBLOB *blob;

	tdsdump_log(TDS_DBG_FUNC, "dbtxtimestamp(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo || !is_blob_col(colinfo))
		return NULL;

	blob = (TDSBLOB *) colinfo->column_data;

	/* test if valid */
	if (!blob->valid_ptr)
		return NULL;

	return (DBBINARY *) blob->timestamp;
}

/**
 * \ingroup dblib_core
 * \brief Get text pointer for a column in the current row.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param column number of the column in the \c SELECT statement, starting at 1.
 * \return text pointer for \a column, may be NULL.
 * \sa dbtxtimestamp(), dbwritetext(). 
 */
DBBINARY *
dbtxptr(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;
	TDSBLOB *blob;

	tdsdump_log(TDS_DBG_FUNC, "dbtxptr(%p, %d)\n", dbproc, column);

	colinfo = dbcolptr(dbproc, column);
	if (!colinfo || !is_blob_col(colinfo))
		return NULL;

	blob = (TDSBLOB *) colinfo->column_data;

	/* test if valid */
	if (!blob->valid_ptr)
		return NULL;

	return (DBBINARY *) blob->textptr;
}

/**
 * \ingroup dblib_core
 * \brief Send text or image data to the server.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param objname table name
 * \param textptr text pointer to be modified, obtained from dbtxptr(). 
 * \param textptrlen \em Ignored.  Supposed to be \c DBTXPLEN.
 * \param timestamp text timestamp to be modified, obtained from dbtxtimestamp() or dbtxtsnewval(), may be \c NULL.
 * \param log \c TRUE if the operation is to be recorded in the transaction log.
 * \param size overall size of the data (in total, not just for this call), in bytes.  A guideline, must not overstate the case.  
 * \param text the chunk of data to write.  
 * \retval SUCCEED everything worked.
 * \retval FAIL not sent, possibly because \a timestamp is invalid or was changed in the database since it was fetched.  
 * \sa dbmoretext(), dbtxptr(), dbtxtimestamp(), dbwritetext(), dbtxtsput().  
 */
RETCODE
dbwritetext(DBPROCESS * dbproc, char *objname, DBBINARY * textptr, DBTINYINT textptrlen, DBBINARY * timestamp, DBBOOL log,
	    DBINT size, BYTE * text)
{
	char textptr_string[35];	/* 16 * 2 + 2 (0x) + 1 */
	char timestamp_string[19];	/* 8 * 2 + 2 (0x) + 1 */
	TDS_INT result_type;

	tdsdump_log(TDS_DBG_FUNC, "dbwritetext(%p, %s, %p, %d, %p, %d)\n", 
				  dbproc, objname, textptr, textptrlen, timestamp, log);
	CHECK_CONN(FAIL);
	CHECK_NULP(objname, "dbwritetext", 2, FAIL);
	CHECK_NULP(textptr, "dbwritetext", 3, FAIL);
	CHECK_NULP(timestamp, "dbwritetext", 5, FAIL);
	CHECK_PARAMETER(size, SYBEZTXT, FAIL);

	if (IS_TDSDEAD(dbproc->tds_socket))
		return FAIL;

	if (textptrlen > DBTXPLEN)
		return FAIL;

	dbconvert(dbproc, SYBBINARY, (BYTE *) textptr, textptrlen, SYBCHAR, (BYTE *) textptr_string, -1);
	dbconvert(dbproc, SYBBINARY, (BYTE *) timestamp, 8, SYBCHAR, (BYTE *) timestamp_string, -1);

	dbproc->dbresults_state = _DB_RES_INIT;

	if (dbproc->tds_socket->state == TDS_PENDING) {
		const TDSRET ret = tds_process_tokens(dbproc->tds_socket, &result_type, NULL, TDS_TOKEN_TRAILING);
		if (ret != TDS_NO_MORE_RESULTS) {
			dbperror(dbproc, SYBERPND, 0);
			dbproc->command_state = DBCMDSENT;
			return FAIL;
		}
	}
	
	if (TDS_FAILED(tds_writetext_start(dbproc->tds_socket, objname, 
		textptr_string, timestamp_string, (log == TRUE), size)))
		return FAIL;

	if (!text) {
		dbproc->text_size = size;
		dbproc->text_sent = 0;
		return SUCCEED;
	}

	tds_writetext_continue(dbproc->tds_socket, text, size);
	tds_writetext_end(dbproc->tds_socket);
	dbproc->text_sent = 0;

	if (dbsqlok(dbproc) == SUCCEED && dbresults(dbproc) == SUCCEED)
		return SUCCEED;
	return FAIL;
}

/**
 * \ingroup dblib_core
 * \brief Fetch part of a text or image value from the server.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param buf \em output: buffer into which text will be placed.
 * \param bufsize size of \a buf, in bytes. 
 * \return 
	- \c >0 count of bytes placed in \a buf.
	- \c  0 end of row.  
	- \c -1 \em error, no result set ready for \a dbproc.
	- \c NO_MORE_ROWS all rows read, no further data. 
 * \sa dbmoretext(), dbnextrow(), dbwritetext(). 
 */
STATUS
dbreadtext(DBPROCESS * dbproc, void *buf, DBINT bufsize)
{
	TDSSOCKET *tds;
	TDSCOLUMN *curcol;
	int cpbytes, bytes_avail;
	TDS_INT result_type;
	TDSRESULTINFO *resinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbreadtext(%p, %p, %d)\n", dbproc, buf, bufsize);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);
	CHECK_NULP(buf, "dbreadtext", 2, -1);

	tds = dbproc->tds_socket;

	if (!tds || !tds->res_info || !tds->res_info->columns[0])
		return -1;

	resinfo = tds->res_info;
	curcol = resinfo->columns[0];

	/*
	 * if the current position is beyond the end of the text
	 * set pos to 0 and return 0 to denote the end of the 
	 * text 
	 */
	if (curcol->column_textpos && curcol->column_textpos >= curcol->column_cur_size) {
		curcol->column_textpos = 0;
		return 0;
	}

	/*
	 * if pos is 0 (first time through or last call exhausted the text)
	 * then read another row
	 */

	if (curcol->column_textpos == 0) {
		const int mask = TDS_STOPAT_ROWFMT|TDS_STOPAT_DONE|TDS_RETURN_ROW|TDS_RETURN_COMPUTE;
		buffer_save_row(dbproc);
		switch (tds_process_tokens(dbproc->tds_socket, &result_type, NULL, mask)) {
		case TDS_SUCCESS:
			if (result_type == TDS_ROW_RESULT || result_type == TDS_COMPUTE_RESULT)
				break;
		case TDS_NO_MORE_RESULTS:
			return NO_MORE_ROWS;
		default:
			return -1;
		}
	}

	/* find the number of bytes to return */
	bytes_avail = curcol->column_cur_size - curcol->column_textpos;
	cpbytes = bytes_avail > bufsize ? bufsize : bytes_avail;
	memcpy(buf, &((TDSBLOB *) curcol->column_data)->textvalue[curcol->column_textpos], cpbytes);
	curcol->column_textpos += cpbytes;
	return cpbytes;
}

/**
 * \ingroup dblib_core
 * \brief Send chunk of a text/image value to the server.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param size count of bytes to send.  
 * \param text textpointer, obtained from dbtxptr.
 * \retval SUCCEED always.
 * \sa dbtxptr(), dbtxtimestamp(), dbwritetext(). 
 * \todo Check return value of called functions and return \c FAIL if appropriate. 
 */
RETCODE
dbmoretext(DBPROCESS * dbproc, DBINT size, const BYTE text[])
{
	tdsdump_log(TDS_DBG_FUNC, "dbmoretext(%p, %d, %p)\n", dbproc, size, text);
	CHECK_CONN(FAIL);
	CHECK_NULP(text, "dbmoretext", 3, FAIL);

	assert(dbproc->text_size >= dbproc->text_sent);

	/* TODO this test should be inside tds_writetext_continue, currently not */
	if (size < 0 || size > dbproc->text_size - dbproc->text_sent)
		return FAIL;

	if (size) {
		if (TDS_FAILED(tds_writetext_continue(dbproc->tds_socket, text, size)))
			return FAIL;
		dbproc->text_sent += size;

		if (dbproc->text_sent == dbproc->text_size) {
			tds_writetext_end(dbproc->tds_socket);
			dbproc->text_sent = 0;
		}
	}

	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Record to a file all SQL commands sent to the server
 * 
 * \param filename name of file to write to. 
 * \remarks Files are named \em filename.n, where n is an integer, starting with 0, and incremented with each callto dbopen().  
 * \sa dbopen(), TDSDUMP environment variable(). 
 */
void
dbrecftos(const char filename[])
{
	char *f;

	tdsdump_log(TDS_DBG_FUNC, "dbrecftos(%s)\n", filename);
	if (filename == NULL) { 
		dbperror(NULL, SYBENULP, 0); 
		return;
	}
	
	f = strdup(filename);
	if (!f) {
		dbperror(NULL, SYBEMEM, 0); 
		return;
	}
	
	tds_mutex_lock(&dblib_mutex);
	free(g_dblib_ctx.recftos_filename);
	g_dblib_ctx.recftos_filename = f;
	g_dblib_ctx.recftos_filenum = 0;
	tds_mutex_unlock(&dblib_mutex);
}

/** \internal
 * \ingroup dblib_internal
 * \brief Get the TDS version in use for \a dbproc.  
 * 
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return a \c DBTDS* token.  
 * \remarks The integer values of the constants are counterintuitive.  
 * \sa DBTDS().
 */
int
dbtds(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbtds(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	if (dbproc->tds_socket) {
		switch (dbproc->tds_socket->conn->tds_version) {
		case 0x402:
			return DBTDS_4_2;
		case 0x406:
			return DBTDS_4_6;
		case 0x500:
			return DBTDS_5_0;
		case 0x700:
			return DBTDS_7_0;
		case 0x701:
			return DBTDS_7_1;
		case 0x702:
			return DBTDS_7_2;
		case 0x703:
			return DBTDS_7_3;
		case 0x704:
			return DBTDS_7_4;
		default:
			return DBTDS_UNKNOWN;
		}
	}
	return -1;
}

/**
 * \ingroup dblib_core
 * \brief See which version of db-lib is in use.
 * 
 * \return null-terminated ASCII string representing the version of db-lib.  
 * \remarks FreeTDS returns the CVS version string of dblib.c.  
 * \sa 
 */
const char *
dbversion()
{
	tdsdump_log(TDS_DBG_FUNC, "dbversion(void)\n");
	return TDS_VERSION_NO;
}

#if defined(DBLIB_UNIMPLEMENTED)
/**
 * \ingroup dblib_core
 * \brief Set the default character set.  
 * 
 * \param charset null-terminated ASCII string, matching a row in master..syscharsets.  
 * \sa dbsetdeflang(), dbsetdefcharset(), dblogin(), dbopen(). 
 * \todo Unimplemented.
 */
RETCODE
dbsetdefcharset(char *charset)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetdefcharset(%s)\n", charset);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbsetdefcharset()\n");
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Ready execution of a registered procedure.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param procedure_name to call.
 * \param namelen size of \a procedure_name, in bytes.
 * \sa dbregparam(), dbregexec(), dbregwatch(), dbreglist(), dbregwatchlist
 * \todo Unimplemented.
 */
RETCODE
dbreginit(DBPROCESS * dbproc, DBCHAR * procedure_name, DBSMALLINT namelen)
{
	tdsdump_log(TDS_DBG_FUNC, "dbreginit(%p, %s, %d)\n", dbproc, procedure_name, namelen);
	CHECK_CONN(FAIL);
	CHECK_NULP(procedure_name, "dbreginit", 2, FAIL);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbreginit()\n");
	return SUCCEED;
}


/**
 * \ingroup dblib_core
 * \brief Get names of Open Server registered procedures.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa dbregparam(), dbregexec(), dbregwatch(), dbreglist(), dbregwatchlist(). 
 * \todo Unimplemented.
 */
RETCODE
dbreglist(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbreglist(%p)\n", dbproc);
	CHECK_CONN(FAIL);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbreglist()\n");
	return SUCCEED;
}


/**
 * \ingroup dblib_core
 * \brief  Describe parameter of registered procedure .
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param param_name
 * \param type \c SYB* datatype. 
 * \param datalen size of \a data.
 * \param data address of buffer holding value for the parameter.  
 * \sa dbreginit(), dbregexec(), dbnpdefine(), dbnpcreate(), dbregwatch(). 
 * \todo Unimplemented.
 */
RETCODE
dbregparam(DBPROCESS * dbproc, char *param_name, int type, DBINT datalen, BYTE * data)
{
	tdsdump_log(TDS_DBG_FUNC, "dbregparam(%p, %s, %d, %d, %p)\n", dbproc, param_name, type, datalen, data);
	CHECK_CONN(FAIL);
	CHECK_NULP(param_name, "dbregparam", 2, FAIL);
	CHECK_NULP(data, "dbregparam", 5, FAIL);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbregparam()\n");
	return SUCCEED;
}


/**
 * \ingroup dblib_core
 * \brief Execute a registered procedure.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param options
 * \sa dbreginit(), dbregparam(), dbregwatch(), dbregnowatch
 * \todo Unimplemented.
 */
RETCODE
dbregexec(DBPROCESS * dbproc, DBUSMALLINT options)
{
	tdsdump_log(TDS_DBG_FUNC, "dbregexec(%p, %d)\n", dbproc, options);
	CHECK_CONN(FAIL);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbregexec()\n");
	return SUCCEED;
}
#endif


/**
 * \ingroup dblib_datetime
 * \brief Get name of a month, in some human language.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param language \em ignored.
 * \param monthnum number of the month, starting with 1.
 * \param shortform set to \c TRUE for a three letter output ("Jan" - "Dec"), else zero.  
 * \return address of null-terminated ASCII string, or \c NULL on error. 
 * \sa db12hour(), dbdateorder(), dbdayname(), DBSETLNATLANG(), dbsetopt().  
 */
const char *
dbmonthname(DBPROCESS * dbproc, char *language, int monthnum, DBBOOL shortform)
{
	static const char shortmon[][4] = {
		"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
	};
	static const char longmon[][12] = {
		"January", "February", "March", "April", "May", "June",
		"July", "August", "September", "October", "November", "December"
	};

	tdsdump_log(TDS_DBG_FUNC, "dbmonthname(%p, %s, %d, %d)\n", dbproc, language, monthnum, shortform);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);
	CHECK_NULP(language, "dbmonthname", 2, NULL);

	if (monthnum < 1 || monthnum > 12)
		return NULL;
	return (shortform) ? shortmon[monthnum - 1] : longmon[monthnum - 1];
}

/**
 * \ingroup dblib_core
 * \brief See if a command caused the current database to change.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return name of new database, if changed, as a null-terminated ASCII string, else \c NULL.

 * \sa dbname(), dbresults(), dbsqlexec(), dbsqlsend(), dbuse().  
 */
char *
dbchange(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbchange(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);

	if (dbproc->envchange_rcv & (1 << (TDS_ENV_DATABASE - 1))) {
		return dbproc->dbcurdb;
	}
	return NULL;
}

/**
 * \ingroup dblib_core
 * \brief Get name of current database.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return current database name, as null-terminated ASCII string.
 * \sa dbchange(), dbuse().  
 */
char *
dbname(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbname(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);
	return dbproc->dbcurdb;
}

/**
 * \ingroup dblib_core
 * \brief Get \c syscharset name of the server character set.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \return name of server's charset, as null-terminated ASCII string.
 * \sa dbcharsetconv(), dbgetcharset(), DBSETLCHARSET().  
 */
char *
dbservcharset(DBPROCESS * dbproc)
{

	tdsdump_log(TDS_DBG_FUNC, "dbservcharset(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, NULL);

	return dbproc->servcharset;
}

/**
 * \ingroup dblib_core
 * \brief Transmit the command buffer to the server.  \em Non-blocking, does not wait for a response.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \retval SUCCEED SQL sent.
 * \retval FAIL protocol problem, unless dbsqlsend() when it's not supposed to be (in which case a db-lib error
 message will be emitted).  
 * \sa dbcmd(), dbfcmd(), DBIORDESC(), DBIOWDESC(), dbnextrow(), dbpoll(), dbresults(), dbsettime(), dbsqlexec(), dbsqlok().  
 */
RETCODE
dbsqlsend(DBPROCESS * dbproc)
{
	TDSSOCKET *tds;
	char *cmdstr;
	TDSRET rc;
	TDS_INT result_type;
	char timestr[256];

	tdsdump_log(TDS_DBG_FUNC, "dbsqlsend(%p)\n", dbproc);
	CHECK_CONN(FAIL);

	tds = dbproc->tds_socket;

	if (tds->state == TDS_PENDING) {

		if (tds_process_tokens(tds, &result_type, NULL, TDS_TOKEN_TRAILING) != TDS_NO_MORE_RESULTS) {
			dbperror(dbproc, SYBERPND, 0);
			dbproc->command_state = DBCMDSENT;
			return FAIL;
		}
	}

	if (dbproc->dboptcmd) {
		if ((cmdstr = dbstring_get(dbproc->dboptcmd)) == NULL) {
			dbperror(dbproc, SYBEASEC, 0); /* Attempt to send an empty command buffer to the server */
			return FAIL;
		}
		rc = tds_submit_query(dbproc->tds_socket, cmdstr);
		free(cmdstr);
		dbstring_free(&(dbproc->dboptcmd));
		if (TDS_FAILED(rc)) {
			return FAIL;
		}
		dbproc->avail_flag = FALSE;
		dbproc->envchange_rcv = 0;
		dbproc->dbresults_state = _DB_RES_INIT;
		while ((rc = tds_process_tokens(tds, &result_type, NULL, TDS_TOKEN_RESULTS))
		       == TDS_SUCCESS);
		if (rc != TDS_NO_MORE_RESULTS) {
			return FAIL;
		}
	}
	dbproc->more_results = TRUE;

	if (dbproc->ftos != NULL) {
		fprintf(dbproc->ftos, "%s\n", dbproc->dbbuf);
		fprintf(dbproc->ftos, "go /* %s */\n", _dbprdate(timestr));
		fflush(dbproc->ftos);
	}

	if (TDS_FAILED(tds_submit_query(dbproc->tds_socket, (char *) dbproc->dbbuf))) {
		return FAIL;
	}
	dbproc->avail_flag = FALSE;
	dbproc->envchange_rcv = 0;
	dbproc->dbresults_state = _DB_RES_INIT;
	dbproc->command_state = DBCMDSENT;
	return SUCCEED;
}

/**
 * \ingroup dblib_core
 * \brief Get user-defined datatype of a compute column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param column Nth column in \a computeid, starting from 1.
 * \returns user-defined datatype of compute column, else -1.
 * \sa dbalttype(), dbcolutype().
 */
DBINT
dbaltutype(DBPROCESS * dbproc, int computeid, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbaltutype(%p, %d, %d)\n", dbproc, computeid, column);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	colinfo = dbacolptr(dbproc, computeid, column, 0);
	if (!colinfo)
		return -1;

	return colinfo->column_usertype;
}

/**
 * \ingroup dblib_core
 * \brief Get size of data in compute column.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param computeid of \c COMPUTE clause to which we're referring. 
 * \param column Nth column in \a computeid, starting from 1.
 * \sa dbadata(), dbadlen(), dbalttype(), dbgetrow(), dbnextrow(), dbnumalts().
 */
DBINT
dbaltlen(DBPROCESS * dbproc, int computeid, int column)
{
	TDSCOLUMN *colinfo;

	tdsdump_log(TDS_DBG_FUNC, "dbaltlen(%p, %d, %d)\n", dbproc, computeid, column);

	colinfo = dbacolptr(dbproc, computeid, column, 0);
	if (!colinfo)
		return -1;

	return colinfo->column_size;

}

/**
 * \ingroup dblib_core
 * \brief See if a server response has arrived.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param milliseconds how long to wait for the server before returning:
 	- \c  0 return immediately.
	- \c -1 do not return until the server responds or a system interrupt occurs.
 * \param ready_dbproc \em output: DBPROCESS for which a response arrived, of \c NULL. 
 * \param return_reason \em output: 
	- \c DBRESULT server responded.
	- \c DBNOTIFICATION registered procedure notification has arrived. dbpoll() the registered handler, if
		any, before it returns.
	- \c DBTIMEOUT \a milliseconds elapsed before the server responded.
	- \c DBINTERRUPT operating-system interrupt occurred before the server responded.
 * \retval SUCCEED everything worked.
 * \retval FAIL a server connection died.
 * \sa  DBIORDESC(), DBRBUF(), dbresults(), dbreghandle(), dbsqlok(). 
 * \todo Unimplemented.
 */
#if defined(DBLIB_UNIMPLEMENTED)
RETCODE
dbpoll(DBPROCESS * dbproc, long milliseconds, DBPROCESS ** ready_dbproc, int *return_reason)
{
	tdsdump_log(TDS_DBG_FUNC, "dbpoll(%p, %ld, %p, %p)\n", dbproc, milliseconds, ready_dbproc, return_reason);
	CHECK_CONN(FAIL);
	CHECK_NULP(ready_dbproc, "dbpoll", 3, FAIL);
	CHECK_NULP(return_reason, "dbpoll", 4, FAIL);
	tdsdump_log(TDS_DBG_FUNC, "UNIMPLEMENTED dbpoll()\n");
	return SUCCEED;
}
#endif

/** \internal
 * \ingroup dblib_internal
 * \brief Get number of the first row in the row buffer.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa DBFIRSTROW(), dbclrbuf(), DBCURROW(), dbgetrow(), DBLASTROW(), dbnextrow(), dbsetopt(). 
 */
DBINT
dbfirstrow(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbfirstrow(%p)\n", dbproc);
	CHECK_CONN(0);
	return buffer_idx2row(&dbproc->row_buf, dbproc->row_buf.tail);
}

/** \internal
 * \ingroup dblib_internal
 * \brief Get number of the last row in the row buffer.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa DBLASTROW(), dbclrbuf(), DBCURROW(), DBFIRSTROW(), dbgetrow(), dbnextrow(), dbsetopt(). 
 */
DBINT
dblastrow(DBPROCESS * dbproc)
{
	int idx;

	tdsdump_log(TDS_DBG_FUNC, "dblastrow(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, 0);
	idx = dbproc->row_buf.head;
	if (dbproc->row_buf.head != dbproc->row_buf.tail) {
		if (--idx < 0) 
			idx = dbproc->row_buf.capacity - 1;
	}
	assert(idx >= 0);
	return buffer_idx2row(&dbproc->row_buf, idx);
}


/** \internal
 * \ingroup dblib_internal
 * \brief Get file descriptor of the socket used by a \c DBPROCESS to read data coming from the server. (!)
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa dbcmd(), DBIORDESC(), DBIOWDESC(), dbnextrow(), dbpoll(), DBRBUF(), dbresults(), dbsqlok(), dbsqlsend().  
 */
int
dbiordesc(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbiordesc(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);
	return (int) tds_get_s(dbproc->tds_socket);
}


/** \internal
 * \ingroup dblib_internal
 * \brief Get file descriptor of the socket used by a \c DBPROCESS to write data coming to the server. (!)
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \sa dbcmd(), DBIORDESC(), DBIOWDESC(), dbnextrow(), dbpoll(), DBRBUF(), dbresults(), dbsqlok(), dbsqlsend().  
 */
int
dbiowdesc(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbiowdesc(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, -1);

	return (int) tds_get_s(dbproc->tds_socket);
}

DBBOOL
dbisavail(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbisavail(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, FALSE);
	return dbproc->avail_flag;
}


/** \internal
 * \ingroup dblib_internal
 * \brief Mark a \c DBPROCESS as "available".
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \remarks Basically bogus.  \c FreeTDS behaves the way Sybase's implementation does, but so what?  
 	Many \c db-lib functions set the \c DBPROCESS to "not available", but only 
	dbsetavail() resets it to "available".  
 * \sa DBISAVAIL(). DBSETAVAIL().
 */
void
dbsetavail(DBPROCESS * dbproc)
{
	tdsdump_log(TDS_DBG_FUNC, "dbsetavail(%p)\n", dbproc);
	CHECK_PARAMETER(dbproc, SYBENULL, );
	dbproc->avail_flag = TRUE;
}


/**
 * \ingroup dblib_core
 * \brief Build a printable string from text containing placeholders for variables.
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param charbuf \em output: buffer that will contain the ASCII null-terminated string built by \c dbstrbuild().
 * \param bufsize size of \a charbuf, in bytes. 
 * \param text null-terminated ASCII string, with \em placeholders for variables. \em A Placeholder is a
 * 	three-byte string, made up of: 
 	- '%' a percent sign
	- 0-9 an integer (designates the argument number to use, starting with 1.)
	- '!' an exclamation point
 * \param formats null-terminated ASCII sprintf-style string.  Has one format specifier for each placeholder in \a text.
 * \remarks Following \a formats are the arguments, the values to substitute for the placeholders.  
 * \sa dbconvert(), dbdatename(), dbdatepart().  
 */
RETCODE
dbstrbuild(DBPROCESS * dbproc, char *charbuf, int bufsize, char *text, char *formats, ...)
{
	va_list ap;
	TDSRET rc;
	int resultlen;

	tdsdump_log(TDS_DBG_FUNC, "dbstrbuild(%p, %s, %d, %s, %s, ...)\n", dbproc, charbuf, bufsize, text, formats);
	CHECK_NULP(charbuf, "dbstrbuild", 2, FAIL);
	CHECK_NULP(text, "dbstrbuild", 4, FAIL);
	CHECK_NULP(formats, "dbstrbuild", 5, FAIL);

	va_start(ap, formats);
	rc = tds_vstrbuild(charbuf, bufsize, &resultlen, text, TDS_NULLTERM, formats, TDS_NULLTERM, ap);
	charbuf[resultlen] = '\0';
	va_end(ap);
	return TDS_SUCCEED(rc) ? SUCCEED : FAIL;
}

static char *
_dbprdate(char *timestr)
{
	time_t currtime = time(NULL);
	
	assert(timestr);

	strcpy(timestr, asctime(gmtime(&currtime)));
	timestr[strlen(timestr) - 1] = '\0';	/* remove newline */
	return timestr;

}

static DBINT
_dbnullable(DBPROCESS * dbproc, int column)
{
	TDSCOLUMN *colinfo;
	TDSRESULTINFO *resinfo;

	assert(dbproc && dbproc->tds_socket);
	resinfo = dbproc->tds_socket->res_info;
	if (!resinfo || column < 1 || column > resinfo->num_cols)
		return FALSE;
	colinfo = resinfo->columns[column - 1];

	if (colinfo->column_nullable) 
		return TRUE;
	return FALSE;
}

/** Returns type in string. Used for debugging purpose */
static const char *
tds_prdatatype(int datatype_token)
{
	switch ((TDS_SERVER_TYPE) datatype_token) {
	case SYBCHAR:		return "SYBCHAR";
	case SYBVARCHAR:	return "SYBVARCHAR";
	case SYBINTN:		return "SYBINTN";
	case SYBINT1:		return "SYBINT1";
	case SYBINT2:		return "SYBINT2";
	case SYBINT4:		return "SYBINT4";
	case SYBINT8:		return "SYBINT8";
	case SYBFLT8:		return "SYBFLT8";
	case SYBDATETIME:	return "SYBDATETIME";
	case SYBBIT:		return "SYBBIT";
	case SYBTEXT:		return "SYBTEXT";
	case SYBNTEXT:		return "SYBNTEXT";
	case SYBIMAGE:		return "SYBIMAGE";
	case SYBMONEY4:		return "SYBMONEY4";
	case SYBMONEY:		return "SYBMONEY";
	case SYBDATETIME4:	return "SYBDATETIME4";
	case SYBREAL:		return "SYBREAL";
	case SYBBINARY:		return "SYBBINARY";
	case SYBVOID:		return "SYBVOID";
	case SYBVARBINARY:	return "SYBVARBINARY";
	case SYBNVARCHAR:	return "SYBNVARCHAR";
	case SYBBITN:		return "SYBBITN";
	case SYBNUMERIC:	return "SYBNUMERIC";
	case SYBDECIMAL:	return "SYBDECIMAL";
	case SYBFLTN:		return "SYBFLTN";
	case SYBMONEYN:		return "SYBMONEYN";
	case SYBDATETIMN:	return "SYBDATETIMN";
	case XSYBCHAR:		return "XSYBCHAR";
	case XSYBVARCHAR:	return "XSYBVARCHAR";
	case XSYBNVARCHAR:	return "XSYBNVARCHAR";
	case XSYBNCHAR:		return "XSYBNCHAR";
	case XSYBVARBINARY:	return "XSYBVARBINARY";
	case XSYBBINARY:	return "XSYBBINARY";
	case SYBLONGBINARY:	return "SYBLONGBINARY";
	case SYBSINT1:		return "SYBSINT1";
	case SYBUINT2:		return "SYBUINT2";
	case SYBUINT4:		return "SYBUINT4";
	case SYBUINT8:		return "SYBUINT8";
	case SYBUNIQUE:		return "SYBUNIQUE";
	case SYBVARIANT:	return "SYBVARIANT";
	case SYBMSXML:		return "SYBMSXML";
	case SYBMSDATE:		return "SYBMSDATE";
	case SYBMSTIME:		return "SYBMSTIME";
	case SYBMSDATETIME2:	return "SYBMSDATETIME2";
	case SYBMSDATETIMEOFFSET: return "SYBMSDATETIMEOFFSET";
	case SYBDATE:		return "SYBDATE";
	case SYBTIME:		return "SYBTIME";
	case SYB5BIGDATETIME:	return "SYBBIGDATETIME";
	case SYB5BIGTIME:	return "SYBBIGTIME";
	case SYBMSUDT:		return "SYBMSUDT";
	case SYBUINT1:		return "SYBUINT1";
	case SYBDATEN:		return "SYBDATEN";
	case SYB5INT8:		return "SYB5INT8";
	case SYBINTERVAL:	return "SYBINTERVAL";
	case SYBTIMEN:		return "SYBTIMEN";
	case SYBUINTN:		return "SYBUINTN";
	case SYBUNITEXT:	return "SYBUNITEXT";
	case SYBXML:		return "SYBXML";
	}
	return "(unknown)";
}
#if 1
void
copy_data_to_host_var(DBPROCESS * dbproc, TDS_SERVER_TYPE srctype, const BYTE * src, DBINT srclen, 
		      BYTE * dest, DBINT destlen,
		      int bindtype, DBINT *indicator)
{
	CONV_RESULT dres;
	DBINT ret;
	int i, len;
	DBINT indicator_value = 0;

	int limited_dest_space = 0;
	TDS_SERVER_TYPE desttype = dblib_bound_type(bindtype);

	tdsdump_log(TDS_DBG_FUNC, "copy_data_to_host_var(%d [%s] len %d => %d [%s] len %d)\n", 
		     srctype, tds_prdatatype(srctype), srclen, desttype, tds_prdatatype(desttype), destlen);
	CHECK_NULP(src, "copy_data_to_host_var", 3, );
	CHECK_NULP(dest, "copy_data_to_host_var", 6, );
	if (desttype == TDS_INVALID_TYPE)
		return;
	/* indicator can be NULL */

	assert(srclen >= 0);

	if (destlen > 0) {
		limited_dest_space = 1;
	}

	/* oft times we are asked to convert a data type to itself */

	if (desttype == SYBNUMERIC) {
		DBNUMERIC *num = NULL;          /* num->scale is unsigned */

		/* only MS, use always source */
		if (bindtype == SRCNUMERICBIND || bindtype == SRCDECIMALBIND) {
			if (is_numeric_type(srctype))
				num = (DBNUMERIC*) src;
			else
				num = (DBNUMERIC*) dest;
		} else if (dbproc->msdblib) {
			/* MS by default use only destination informations */
			num = (DBNUMERIC*) dest;
		} else {
			/* Sybase, dbbind means source or default */
			/* TODO if dbbind_ps is used is more complicated */
			if (is_numeric_type(srctype))
				num = (DBNUMERIC*) src;
		}
		if (!num) {
			dres.n.precision = 18;
			dres.n.scale = 0;
		} else {
			dres.n.precision = num->precision;
			dres.n.scale = num->scale;
		}
	} else if ((srctype == desttype) ||
		(is_similar_type(srctype, desttype))) {

		tdsdump_log(TDS_DBG_INFO1, "copy_data_to_host_var() srctype == desttype\n");
		switch (desttype) {

		case SYBBINARY:
		case SYBIMAGE:
			if (srclen > destlen && destlen >= 0) {
				dbperror(dbproc, SYBECOFL, 0);
			} else {
				memcpy(dest, src, srclen);
				if (srclen < destlen)
					memset(dest + srclen, 0, destlen - srclen);
			}
			break;

		case SYBCHAR:
		case SYBVARCHAR:
		case SYBTEXT:

			switch (bindtype) {
				case NTBSTRINGBIND: /* strip trailing blanks, null term */
					while (srclen && src[srclen - 1] == ' ') {
						--srclen;
					}
					if (limited_dest_space) {
						if (srclen + 1 > destlen) {
							dbperror(dbproc, SYBECOFL, 0);
							indicator_value = srclen + 1;
							srclen = destlen - 1;
						}
					}
					memcpy(dest, src, srclen);
					dest[srclen] = '\0';
					break;
				case STRINGBIND:   /* pad with blanks, NUL term */
					if (limited_dest_space) {
						if (srclen + 1 > destlen) {
							dbperror(dbproc, SYBECOFL, 0);
							indicator_value = srclen + 1;
							srclen = destlen - 1;
						}
					} else {
						destlen = srclen + 1;
					}
					memcpy(dest, src, srclen);
					if (srclen < destlen)
						memset(dest + srclen, ' ', destlen - srclen - 1);
					dest[destlen - 1] = '\0';
					break;
				case CHARBIND:   /* pad with blanks, NO NUL term */
					if (limited_dest_space) {
						if (srclen > destlen) {
							dbperror(dbproc, SYBECOFL, 0);
							indicator_value = srclen;
							srclen = destlen;
						}
					} else {
						destlen = srclen; 
					}
					memcpy(dest, src, srclen);
					if (srclen < destlen)
						memset(dest + srclen, ' ', destlen - srclen);
					break;
				case VARYCHARBIND: /* strip trailing blanks, NO NUL term */
					if (limited_dest_space) {
						if (srclen > destlen) {
							dbperror(dbproc, SYBECOFL, 0);
							indicator_value = srclen;
							srclen = destlen;
						}
					}
					memcpy(((DBVARYCHAR *)dest)->str, src, srclen);
					((DBVARYCHAR *)dest)->len = srclen;
					break;
			} 
			break;
		case SYBINT1:
		case SYBINT2:
		case SYBINT4:
		case SYBINT8:
		case SYBFLT8:
		case SYBREAL:
		case SYBBIT:
		case SYBBITN:
		case SYBMONEY:
		case SYBMONEY4:
		case SYBDATETIME:
		case SYBDATETIME4:
		case SYBDATE:
		case SYBTIME:
		case SYB5BIGDATETIME:
		case SYB5BIGTIME:
		case SYBUNIQUE:
			ret = tds_get_size_by_type(desttype);
			memcpy(dest, src, ret);
			break;

		case SYBMSDATE:
		case SYBMSTIME:
		case SYBMSDATETIME2:
		case SYBMSDATETIMEOFFSET:
			ret = sizeof(TDS_DATETIMEALL);
			memcpy(dest, src, ret);
			break;

		default:
			break;
		}
		if (indicator)
			*indicator = indicator_value;

		return;

	} /* end srctype == desttype */

	len = tds_convert(g_dblib_ctx.tds_ctx, srctype, (const TDS_CHAR *) src, srclen, desttype, &dres);

	tdsdump_log(TDS_DBG_INFO1, "copy_data_to_host_var(): tds_convert returned %d\n", len);

	if (len < 0) {
		_dblib_convert_err(dbproc, len);
		return;
	}

	switch (desttype) {
	case SYBVARBINARY:
	case SYBBINARY:
	case SYBIMAGE:
		if (bindtype == VARYBINBIND) {
			if (limited_dest_space) {
				if (len > sizeof(((DBVARYBIN *)dest)->array)) {
					dbperror(dbproc, SYBECOFL, 0);
					indicator_value = len;
					len = sizeof(((DBVARYBIN *)dest)->array);
				}
			}
			memcpy(((DBVARYBIN *)dest)->array, dres.c, len);
			((DBVARYBIN *)dest)->len = len;
		} else {
			if (len > destlen && destlen >= 0) {
				dbperror(dbproc, SYBECOFL, 0);
			} else {
				memcpy(dest, dres.ib, len);
				if (len < destlen)
					memset(dest + len, 0, destlen - len);
			}
		}
		TDS_ZERO_FREE(dres.ib);
		break;
	case SYBINT1:
	case SYBINT2:
	case SYBINT4:
	case SYBINT8:
	case SYBFLT8:
	case SYBREAL:
	case SYBBIT:
	case SYBBITN:
	case SYBMONEY:
	case SYBMONEY4:
	case SYBDATETIME:
	case SYBDATETIME4:
	case SYBDATE:
	case SYBTIME:
	case SYBNUMERIC:
	case SYBDECIMAL:
	case SYBUNIQUE:
	case SYBMSDATE:
	case SYBMSTIME:
	case SYB5BIGDATETIME:
	case SYB5BIGTIME:
	case SYBMSDATETIME2:
	case SYBMSDATETIMEOFFSET:
		memcpy(dest, &(dres.ti), len);
		break;
	case SYBCHAR:
	case SYBVARCHAR:
	case SYBTEXT:
		tdsdump_log(TDS_DBG_INFO1, "copy_data_to_host_var() outputs %d bytes char data destlen = %d \n", len, destlen);
		switch (bindtype) {
			case NTBSTRINGBIND: /* strip trailing blanks, null term */
				while (len && dres.c[len - 1] == ' ') {
					--len;
				}
				if (limited_dest_space) {
					if (len + 1 > destlen) {
						dbperror(dbproc, SYBECOFL, 0);
						len = destlen - 1;
					}
				}
				memcpy(dest, dres.c, len);
				dest[len] = '\0';
				break;
			case STRINGBIND:   /* pad with blanks, null term */
				if (limited_dest_space) {
					if (len + 1 > destlen) {
						dbperror(dbproc, SYBECOFL, 0);
						len = destlen - 1;
					}
				} else {
					destlen = len + 1;
				}
				memcpy(dest, dres.c, len);
				if (len < destlen)
					memset(dest + len, ' ', destlen - len - 1);
				dest[destlen - 1] = '\0';
				break;
			case CHARBIND:   /* pad with blanks, NO null term */
				if (limited_dest_space) {
					if (len > destlen) {
						dbperror(dbproc, SYBECOFL, 0);
						indicator_value = len;
						len = destlen;
					}
				} else {
					destlen = len; 
				}
				memcpy(dest, dres.c, len);
				if (len < destlen)
					memset(dest + len, ' ', destlen - len);
				break;
			case VARYCHARBIND: /* strip trailing blanks, NO null term */
				if (limited_dest_space) {
					if (len > sizeof(((DBVARYCHAR *)dest)->str)) {
						dbperror(dbproc, SYBECOFL, 0);
						indicator_value = len;
						len = sizeof(((DBVARYCHAR *)dest)->str);
					}
				} 
				memcpy(((DBVARYCHAR *)dest)->str, dres.c, len);
				((DBVARYCHAR *)dest)->len = len;
				break;
		} 

		free(dres.c);
		break;
	default:
		tdsdump_log(TDS_DBG_INFO1, "error: copy_data_to_host_var(): unrecognized desttype %d \n", desttype);
		break;

	}
	if (indicator)
		*indicator = indicator_value;

	return;
}
#endif

/** \internal
 * \ingroup dblib_internal
 * \remarks member msgno Vendor-defined message number
 * \remarks member severity Is passed to the error handler 
 * \remarks member msgtext Text of message
 */
typedef struct _dblib_error_message 
{
	DBINT msgno;
	int severity;
	const char *msgtext;
} DBLIB_ERROR_MESSAGE;

/*
 * The msgtext member holds up to two strings.  The first one is the message text, which may contain placeholders.  
 * The second one, if it exists, is the format string for dbstrbuild().  Messages containing no placeholders still need
 * an extra NULL to indicate a zero-length format string. 
 */
static const DBLIB_ERROR_MESSAGE dblib_error_messages[] = 
	{ { SYBEVERDOWN, 	   EXINFO,	"TDS version downgraded to 7.1!\0" }
	, { SYBEICONVIU,     EXCONVERSION,	"Some character(s) could not be converted into client's character set\0" }
	, { SYBEICONVAVAIL,  EXCONVERSION,	"Character set conversion is not available between client character set '%1!' and "
						"server character set '%2!'\0%s %s" }
	, { SYBEICONVO,      EXCONVERSION,	"Error converting characters into server's character set. Some character(s) could "
						"not be converted\0" }
	, { SYBEICONVI,      EXCONVERSION,	"Some character(s) could not be converted into client's character set.  Unconverted "
						"bytes were changed to question marks ('?')\0" }
	, { SYBEICONV2BIG,   EXCONVERSION,	"Buffer overflow converting characters from client into server's character set\0" }
	
	
	, { SYBEPORT, 	   	   EXUSER,	"Both port and instance specified\0" }
	, { SYBETDSVER, 	   EXUSER,	"Cannot bcp with TDSVER < 5.0\0" }
	, { SYBEAAMT,           EXPROGRAM,	"User attempted a dbaltbind with mismatched column and variable types\0" }
	, { SYBEABMT,           EXPROGRAM,	"User attempted a dbbind with mismatched column and variable types\0" }
	, { SYBEABNC,           EXPROGRAM,	"Attempt to bind to a non-existent column\0" }
	, { SYBEABNP,           EXPROGRAM,	"Attempt to bind using NULL pointers\0" }
	, { SYBEABNV,           EXPROGRAM,	"Attempt to bind to a NULL program variable\0" }
	, { SYBEACNV,        EXCONVERSION,	"Attempt to do data-conversion with NULL destination variable.\0" }
	, { SYBEADST,       EXCONSISTENCY,	"International Release: Error in attempting to determine the size of a pair of "
						"translation tables\0" }
	, { SYBEAICF,       EXCONSISTENCY,	"International Release: Error in attempting to install custom format\0" }
	, { SYBEALTT,       EXCONSISTENCY,	"International Release: Error in attempting to load a pair of translation tables\0" }
	, { SYBEAOLF,          EXRESOURCE,	"International Release: Error in attempting to open a localization file\0" }
	, { SYBEAPCT,       EXCONSISTENCY,	"International Release: Error in attempting to perform a character set translation\0" }
	, { SYBEAPUT,           EXPROGRAM,	"Attempt to print unknown token\0" }
	, { SYBEARDI,          EXRESOURCE,	"International Release: Error in attempting to read datetime information from a "
						"localization file\0" }
	, { SYBEARDL,          EXRESOURCE,	"International Release: Error in attempting to read the dblib.loc localization file\0" }
	, { SYBEASEC,           EXPROGRAM,	"Attempt to send an empty command buffer to the server\0" }
	, { SYBEASNL,           EXPROGRAM,	"Attempt to set fields in a null LOGINREC\0" }
	, { SYBEASTL,           EXPROGRAM,	"Synchronous I/O attempted at AST level\0" }
	, { SYBEASUL,           EXPROGRAM,	"Attempt to set unknown LOGINREC field\0" }
	, { SYBEAUTN,           EXPROGRAM,	"Attempt to update the timestamp of a table that has no timestamp column\0" }
	, { SYBEBADPK,             EXINFO,	"Packet size of %1! not supported -- size of %2! used instead!\0%d %d" }
	, { SYBEBBCI,              EXINFO,	"Batch successfully bulk copied to the server\0" }
	, { SYBEBBL,            EXPROGRAM,	"Bad bindlen parameter passed to dbsetnull\0" }
	, { SYBEBCBC,           EXPROGRAM,	"bcp_columns must be called before bcp_colfmt and bcp_colfmt_ps\0" }
	, { SYBEBCBNPR,         EXPROGRAM,	"bcp_bind: if varaddr is NULL, prefixlen must be 0 "
						"and no terminator should be specified\0" }
	, { SYBEBCBNTYP,        EXPROGRAM,	"bcp_bind: if varaddr is NULL and varlen greater than 0, the table column type "
						"must be SYBTEXT or SYBIMAGE and the program variable type must be SYBTEXT, SYBCHAR, "
						"SYBIMAGE or SYBBINARY\0" }
	, { SYBEBCBPREF,        EXPROGRAM,	"Illegal prefix length. Legal values are 0, 1, 2 or 4\0" }
	, { SYBEBCFO,              EXUSER,	"bcp host files must contain at least one column\0" }
	, { SYBEBCHLEN,         EXPROGRAM,	"host_collen should be greater than or equal to -1\0" }
	, { SYBEBCIS,       EXCONSISTENCY,	"Attempt to bulk copy an illegally-sized column value to the server\0" }
	, { SYBEBCIT,           EXPROGRAM,	"It is illegal to use BCP terminators with program variables other than SYBCHAR, "
						"SYBBINARY, SYBTEXT, or SYBIMAGE\0" }
	, { SYBEBCITBLEN,       EXPROGRAM,	"bcp_init: tblname parameter is too long\0" }
	, { SYBEBCITBNM,        EXPROGRAM,	"bcp_init: tblname parameter cannot be NULL\0" }
	, { SYBEBCMTXT,         EXPROGRAM,	"bcp_moretext may be used only when there is at least one text or image column in "
						"the server table\0" }
	, { SYBEBCNL,          EXNONFATAL,	"Negative length-prefix found in BCP data-file\0" }
	, { SYBEBCNN,              EXUSER,	"Attempt to bulk copy a NULL value into a Server column "
						"which does not accept null values\0" }
	, { SYBEBCNT,              EXUSER,	"Attempt to use Bulk Copy with a non-existent Server table\0" }
	, { SYBEBCOR,       EXCONSISTENCY,	"Attempt to bulk copy an oversized row to the server\0" }
	, { SYBEBCPB,           EXPROGRAM,	"bcp_bind, bcp_moretext and bcp_sendrow may not be used after bcp_init has been "
						"passed a non-NULL input file name\0" }
	, { SYBEBCPCTYP,        EXPROGRAM,	"bcp_colfmt: If table_colnum is 0, host_type cannot be 0\0" }
	, { SYBEBCPI,           EXPROGRAM,	"bcp_init must be called before any other bcp routines\0" }
	, { SYBEBCPN,           EXPROGRAM,	"bcp_bind, bcp_collen, bcp_colptr, bcp_moretext and bcp_sendrow may be used only "
						"after bcp_init has been called with the copy direction set to DB_IN\0" }
	, { SYBEBCPREC,        EXNONFATAL,	"Column %1!: Illegal precision value encountered\0%d" }
	, { SYBEBCPREF,         EXPROGRAM,	"Illegal prefix length. Legal values are -1, 0, 1, 2 or 4\0" }
	, { SYBEBCRE,          EXNONFATAL,	"I/O error while reading bcp datafile\0" }
	, { SYBEBCRO,              EXINFO,	"The BCP hostfile '%1!' contains only %2! rows. It was impossible to read the "
						"requested %3! rows\0%s %d %d" }
	, { SYBEBCSA,              EXUSER,	"The BCP hostfile '%1!' contains only %2! rows. "
						"Skipping all of these rows is not allowed\0%s %d" }
	, { SYBEBCSET,      EXCONSISTENCY,	"Unknown character-set encountered\0" }
	, { SYBEBCSI,           EXPROGRAM,	"Host-file columns may be skipped only when copying into the Server\0" }
	, { SYBEBCSNDROW,       EXPROGRAM,	"bcp_sendrow may not be called unless all text data for the previous row has been "
						"sent using bcp_moretext\0" }
	, { SYBEBCSNTYP,        EXPROGRAM,	"column number %1!: if varaddr is NULL and varlen greater than 0, the table column "
						"type must be SYBTEXT or SYBIMAGE and the program variable type must be SYBTEXT, "
						"SYBCHAR, SYBIMAGE or SYBBINARY\0%d" }
	, { SYBEBCUC,          EXRESOURCE,	"bcp: Unable to close host datafile\0" }
	, { SYBEBCUO,          EXRESOURCE,	"bcp: Unable to open host datafile\0" }
	, { SYBEBCVH,           EXPROGRAM,	"bcp_exec may be called only after bcp_init has been passed a valid host file\0" }
	, { SYBEBCVLEN,         EXPROGRAM,	"varlen should be greater than or equal to -1\0" }
	, { SYBEBCWE,          EXNONFATAL,	"I/O error while writing bcp datafile\0" }
	, { SYBEBDIO,           EXPROGRAM,	"Bad bulk copy direction. Must be either IN or OUT\0" }
	, { SYBEBEOF,          EXNONFATAL,	"Unexpected EOF encountered in bcp datafile\0" }
	, { SYBEBIHC,           EXPROGRAM,	"Incorrect host-column number found in bcp format file\0" }
	, { SYBEBIVI,           EXPROGRAM,	"bcp_columns, bcp_colfmt and bcp_colfmt_ps may be used only after bcp_init has been "
						"passed a valid input file\0" }
	, { SYBEBNCR,           EXPROGRAM,	"Attempt to bind user variable to a non-existent compute row\0" }
	, { SYBEBNUM,           EXPROGRAM,	"Bad numbytes parameter passed to dbstrcpy\0" }
	, { SYBEBPKS,           EXPROGRAM,	"In DBSETLPACKET, the packet size parameter must be between 0 and 999999\0" }
	, { SYBEBPREC,          EXPROGRAM,	"Illegal precision specified\0" }
	, { SYBEBPROBADDEF, EXCONSISTENCY,	"bcp protocol error: illegal default column id received\0" }
	, { SYBEBPROCOL,    EXCONSISTENCY,	"bcp protocol error: returned column count differs from the actual number of "
						"columns received\0" }
	, { SYBEBPRODEF,    EXCONSISTENCY,	"bcp protocol error: expected default information and got none\0" }
	, { SYBEBPRODEFID,  EXCONSISTENCY,	"bcp protocol error: default column id and actual column id are not same\0" }
	, { SYBEBPRODEFTYP, EXCONSISTENCY,	"bcp protocol error: default value datatype differs from column datatype\0" }
	, { SYBEBPROEXTDEF, EXCONSISTENCY,	"bcp protocol error: more than one row of default information received\0" }
	, { SYBEBPROEXTRES, EXCONSISTENCY,	"bcp protocol error: unexpected set of results received\0" }
	, { SYBEBPRONODEF,  EXCONSISTENCY,	"bcp protocol error: default value received for column that does not have default\0" }
	, { SYBEBPRONUMDEF, EXCONSISTENCY,	"bcp protocol error: expected number of defaults differs from the actual number of "
						"defaults received\0" }
	, { SYBEBRFF,          EXRESOURCE,	"I/O error while reading bcp format file\0" }
	, { SYBEBSCALE,         EXPROGRAM,	"Illegal scale specified\0" }
	, { SYBEBTMT,           EXPROGRAM,	"Attempt to send too much text data via the bcp_moretext call\0" }
	, { SYBEBTOK,              EXCOMM,	"Bad token from the server: Datastream processing out of sync\0" }
	, { SYBEBTYP,           EXPROGRAM,	"Unknown bind type passed to DB-Library function\0" }
	, { SYBEBTYPSRV,        EXPROGRAM,	"Datatype is not supported by the server\0" }
	, { SYBEBUCE,          EXRESOURCE,	"bcp: Unable to close error file\0" }
	, { SYBEBUCF,           EXPROGRAM,	"bcp: Unable to close format file\0" }
	, { SYBEBUDF,           EXPROGRAM,	"bcp: Unrecognized datatype found in format file\0" }
	, { SYBEBUFF,           EXPROGRAM,	"bcp: Unable to create format file\0" }
	, { SYBEBUFL,       EXCONSISTENCY,	"DB-Library internal error-send buffer length corrupted\0" }
	, { SYBEBUOE,          EXRESOURCE,	"bcp: Unable to open error file\0" }
	, { SYBEBUOF,           EXPROGRAM,	"bcp: Unable to open format file\0" }
	, { SYBEBWEF,          EXNONFATAL,	"I/O error while writing bcp error file\0" }
	, { SYBEBWFF,          EXRESOURCE,	"I/O error while writing bcp format file\0" }
	, { SYBECAP,               EXCOMM,	"DB-Library capabilities not accepted by the Server\0" }
	, { SYBECAPTYP,            EXCOMM,	"Unexpected capability type in CAPABILITY datastream\0" }
	, { SYBECDNS,       EXCONSISTENCY,	"Datastream indicates that a compute column is derived from a non-existent select "
						"list member\0" }
	, { SYBECDOMAIN,     EXCONVERSION,	"Source field value is not within the domain of legal values\0" }
	, { SYBECINTERNAL,   EXCONVERSION,	"Internal Conversion error\0" }
	, { SYBECLOS,              EXCOMM,	"Error in closing network connection\0" }
	, { SYBECLPR,        EXCONVERSION,	"Data conversion resulted in loss of precision\0" }
	, { SYBECNOR,           EXPROGRAM,	"Column number out of range\0" }
	, { SYBECNOV,        EXCONVERSION,	"Attempt to set variable to NULL resulted in overflow\0" }
	, { SYBECOFL,        EXCONVERSION,	"Data conversion resulted in overflow\0" }
	, { SYBECONN,              EXCOMM,	"Unable to connect: Adaptive Server is unavailable or does not exist\0" }
	, { SYBECRNC,           EXPROGRAM,	"The current row is not a result of compute clause %1!, so it is illegal to attempt "
						"to extract that data from this row\0%d" }
	, { SYBECRSAGR,         EXPROGRAM,	"Aggregate functions are not allowed in a cursor statement\0" }
	, { SYBECRSBROL,        EXPROGRAM,	"Backward scrolling cannot be used in a forward scrolling cursor\0" }
	, { SYBECRSBSKEY,       EXPROGRAM,	"Keyset cannot be scrolled backward in mixed cursors with a previous fetch type\0" }
	, { SYBECRSBUFR,        EXPROGRAM,	"Row buffering should not be turned on when using cursor APIs\0" }
	, { SYBECRSDIS,         EXPROGRAM,	"Cursor statement contains one of the disallowed phrases compute, union, for "
						"browse, or select into\0" }
	, { SYBECRSFLAST,       EXPROGRAM,	"Fetch type LAST requires fully keyset driven cursors\0" }
	, { SYBECRSFRAND,       EXPROGRAM,	"Fetch types RANDOM and RELATIVE can only be used within the keyset of keyset "
						"driven cursors\0" }
	, { SYBECRSFROWN,       EXPROGRAM,	"Row number to be fetched is outside valid range\0" }
	, { SYBECRSFTYPE,      EXRESOURCE,	"Unknown fetch type\0" }
	, { SYBECRSINV,         EXPROGRAM,	"Invalid cursor statement\0" }
	, { SYBECRSINVALID,    EXRESOURCE,	"The cursor handle is invalid\0" }
	, { SYBECRSMROWS,      EXRESOURCE,	"Multiple rows are returned, only one is expected while retrieving dbname\0" }
	, { SYBECRSNOBIND,      EXPROGRAM,	"Cursor bind must be called prior to dbcursor invocation\0" }
	, { SYBECRSNOCOUNT,     EXPROGRAM,	"The DBNOCOUNT option should not be turned on "
						"when doing updates or deletes with dbcursor\0" }
	, { SYBECRSNOFREE,      EXPROGRAM,	"The DBNOAUTOFREE option should not be turned on when using cursor APIs\0" }
	, { SYBECRSNOIND,       EXPROGRAM,	"One of the tables involved in the cursor statement does not have a unique index\0" }
	, { SYBECRSNOKEYS,     EXRESOURCE,	"The entire keyset must be defined for KEYSET type cursors\0" }
	, { SYBECRSNOLEN,      EXRESOURCE,	"No unique index found\0" }
	, { SYBECRSNOPTCC,     EXRESOURCE,	"No OPTCC was found\0" }
	, { SYBECRSNORDER,     EXRESOURCE,	"The order of clauses must be from, where, and order by\0" }
	, { SYBECRSNORES,       EXPROGRAM,	"Cursor statement generated no results\0" }
	, { SYBECRSNROWS,      EXRESOURCE,	"No rows returned, at least one is expected\0" }
	, { SYBECRSNOTABLE,    EXRESOURCE,	"Table name is NULL\0" }
	, { SYBECRSNOUPD,       EXPROGRAM,	"Update or delete operation did not affect any rows\0" }
	, { SYBECRSNOWHERE,     EXPROGRAM,	"A WHERE clause is not allowed in a cursor update or insert\0" }
	, { SYBECRSNUNIQUE,    EXRESOURCE,	"No unique keys associated with this view\0" }
	, { SYBECRSORD,         EXPROGRAM,	"Only fully keyset driven cursors can have order by, group by, or having phrases\0" }
	, { SYBECRSRO,          EXPROGRAM,	"Data locking or modifications cannot be made in a READONLY cursor\0" }
	, { SYBECRSSET,         EXPROGRAM,	"A SET clause is required for a cursor update or insert\0" }
	, { SYBECRSTAB,         EXPROGRAM,	"Table name must be determined in operations involving data locking or modifications\0" }
	, { SYBECRSVAR,        EXRESOURCE,	"There is no valid address associated with this bind\0" }
	, { SYBECRSVIEW,        EXPROGRAM,	"A view cannot be joined with another table or a view in a cursor statement\0" }
	, { SYBECRSVIIND,       EXPROGRAM,	"The view used in the cursor statement does not include all the unique index "
						"columns of the underlying tables\0" }
	, { SYBECRSUPDNB,       EXPROGRAM,	"Update or insert operations cannot use bind variables when binding type is NOBIND\0" }
	, { SYBECRSUPDTAB,      EXPROGRAM,	"Update or insert operations using bind variables require single table cursors\0" }
	, { SYBECSYN,        EXCONVERSION,	"Attempt to convert data stopped by syntax error in source field\0" }
	, { SYBECUFL,        EXCONVERSION,	"Data conversion resulted in underflow\0" }
	, { SYBECWLL,           EXPROGRAM,	"Attempt to set column width less than 1\0" }
	, { SYBEDBPS,          EXRESOURCE,	"Maximum number of DBPROCESSes already allocated\0" }
	, { SYBEDDNE,              EXINFO,	"DBPROCESS is dead or not enabled\0" }
	, { SYBEDIVZ,              EXUSER,	"Attempt to divide by $0.00 in function %1!\0%s" }
	, { SYBEDNTI,           EXPROGRAM,	"Attempt to use dbtxtsput to put a new text timestamp into a column whose datatype "
						"is neither SYBTEXT nor SYBIMAGE\0" }
	, { SYBEDPOR,           EXPROGRAM,	"Out-of-range datepart constant\0" }
	, { SYBEDVOR,           EXPROGRAM,	"Day values must be between 1 and 7\0" }
	, { SYBEECAN,              EXINFO,	"Attempted to cancel unrequested event notification\0" }
	, { SYBEEINI,              EXINFO,	"Must call dbreginit before dbregexec\0" }
	, { SYBEETD,            EXPROGRAM,	"Failure to send the expected amount of TEXT or IMAGE data via dbmoretext\0" }
	, { SYBEEUNR,              EXCOMM,	"Unsolicited event notification received\0" }
	, { SYBEEVOP,              EXINFO,	"Called dbregwatch with a bad options parameter\0" }
	, { SYBEEVST,              EXINFO,	"Must initiate a transaction before calling dbregparam\0" }
	, { SYBEFCON,              EXCOMM,	"Adaptive Server connection failed\0" }
	, { SYBEFRES,             EXFATAL,	"Challenge-Response function failed\0" }
	, { SYBEFSHD,          EXRESOURCE,	"Error in attempting to find the Sybase home directory\0" }
	, { SYBEFUNC,           EXPROGRAM,	"Functionality not supported at the specified version level\0" }
	, { SYBEICN,            EXPROGRAM,	"Invalid computeid or compute column number\0" }
	, { SYBEIDCL,       EXCONSISTENCY,	"Illegal datetime column length returned by Adaptive Server. Legal datetime lengths "
						"are 4 and 8 bytes\0" }
	, { SYBEIDECCL,     EXCONSISTENCY,	"Invalid decimal column length returned by the server\0" }
	, { SYBEIFCL,       EXCONSISTENCY,	"Illegal floating-point column length returned by Adaptive Server. Legal "
						"floating-point lengths are 4 and 8 bytes\0" }
	, { SYBEIFNB,           EXPROGRAM,	"Illegal field number passed to bcp_control\0" }
	, { SYBEIICL,       EXCONSISTENCY,	"Illegal integer column length returned by Adaptive Server. Legal integer lengths "
						"are 1, 2, and 4 bytes\0" }
	, { SYBEIMCL,       EXCONSISTENCY,	"Illegal money column length returned by Adaptive Server. Legal money lengths are 4 "
						"and 8 bytes\0" }
	, { SYBEINLN,              EXUSER,	"Interface file: unexpected end-of-line\0" }
	, { SYBEINTF,              EXUSER,	"Server name not found in configuration files\0" }
	, { SYBEINUMCL,     EXCONSISTENCY,	"Invalid numeric column length returned by the server\0" }
	, { SYBEIPV,               EXINFO,	"%1! is an illegal value for the %2! parameter of %3!\0%d %s %s" }
	, { SYBEISOI,       EXCONSISTENCY,	"International Release: Invalid sort-order information found\0" }
	, { SYBEISRVPREC,   EXCONSISTENCY,	"Illegal precision value returned by the server\0" }
	, { SYBEISRVSCL,    EXCONSISTENCY,	"Illegal scale value returned by the server\0" }
	, { SYBEITIM,           EXPROGRAM,	"Illegal timeout value specified\0" }
	, { SYBEIVERS,          EXPROGRAM,	"Illegal version level specified\0" }
	, { SYBEKBCI,              EXINFO,	"1000 rows sent to the server\0" }
	, { SYBEKBCO,              EXINFO,	"1000 rows successfully bulk copied to host file\0" }
	, { SYBEMEM,           EXRESOURCE,	"Unable to allocate sufficient memory\0" }
	, { SYBEMOV,               EXUSER,	"Money arithmetic resulted in overflow in function %1!\0%s" }
	, { SYBEMPLL,              EXUSER,	"Attempt to set maximum number of DBPROCESSes lower than 1\0" }
	, { SYBEMVOR,           EXPROGRAM,	"Month values must be between 1 and 12\0" }
	, { SYBENBUF,              EXINFO,	"Called dbsendpassthru with a NULL buf parameter\0" }
	, { SYBENBVP,           EXPROGRAM,	"Cannot pass dbsetnull a NULL bindval pointer\0" }
	, { SYBENDC,            EXPROGRAM,	"Cannot have negative component in date in numeric form\0" }
	, { SYBENDTP,           EXPROGRAM,	"Called dbdatecrack with NULL datetime parameter\0" }
	, { SYBENEG,               EXCOMM,	"Negotiated login attempt failed\0" }
	, { SYBENHAN,              EXINFO,	"Called dbrecvpassthru with a NULL handle parameter\0" }
	, { SYBENMOB,           EXPROGRAM,	"No such member of order by clause\0" }
	, { SYBENOEV,              EXINFO,	"DBPOLL can not be called when registered procedure notifications have been disabled\0" }
	, { SYBENPRM,           EXPROGRAM,	"NULL parameter not allowed for this dboption\0" }
	, { SYBENSIP,           EXPROGRAM,	"Negative starting index passed to dbstrcpy\0" }
	, { SYBENTLL,              EXUSER,	"Name too long for LOGINREC field\0" }
	, { SYBENTTN,           EXPROGRAM,	"Attempt to use dbtxtsput to put a new text timestamp into a non-existent data row\0" }
	, { SYBENULL,              EXINFO,	"NULL DBPROCESS pointer passed to DB-Library\0" }
	, { SYBENULP,           EXPROGRAM,	"Called %1! with parameter %2! NULL\0%s %d" }
	, { SYBENXID,          EXNONFATAL,	"The Server did not grant us a distributed-transaction ID\0" }
	, { SYBEONCE,           EXPROGRAM,	"Function can be called only once\0" }
	, { SYBEOOB,               EXCOMM,	"Error in sending out-of-band data to the server\0" }
	, { SYBEOPIN,          EXNONFATAL,	"Could not open interface file\0" }
	, { SYBEOPNA,          EXNONFATAL,	"Option is not available with current server\0" }
	, { SYBEOREN,              EXINFO,	"International Release: Warning: an out-of-range error-number was encountered in "
						"dblib.loc. The maximum permissible error-number is defined as DBERRCOUNT in sybdb.h\0" }
	, { SYBEORPF,              EXUSER,	"Attempt to set remote password would overflow "
						"the login record's remote password field\0" }
	, { SYBEPOLL,              EXINFO,	"There is already an active dbpoll\0" }
	, { SYBEPRTF,              EXINFO,	"dbtracestring may only be called from a printfunc\0" }
	, { SYBEPWD,               EXUSER,	"Login incorrect\0" }
	, { SYBERDCN,        EXCONVERSION,	"Requested data conversion does not exist\0" }
	, { SYBERDNR,           EXPROGRAM,	"Attempt to retrieve data from a non-existent row\0" }
	, { SYBEREAD,              EXCOMM,	"Read from the server failed\0" }
	, { SYBERESP,           EXPROGRAM,	"Response function address passed to dbresponse must be non-NULL\0" }
	, { SYBERPCS,              EXINFO,	"Must call dbrpcinit before dbrpcparam or dbrpcsend\0" }
	, { SYBERPIL,           EXPROGRAM,	"It is illegal to pass -1 to dbrpcparam for the datalen of parameters which are of "
						"type SYBCHAR, SYBVARCHAR, SYBBINARY, or SYBVARBINARY\0" }
	, { SYBERPNA,          EXNONFATAL,	"The RPC facility is available only when using a server whose version number is 4.0 "
						"or later\0" }
	, { SYBERPND,           EXPROGRAM,	"Attempt to initiate a new Adaptive Server operation with results pending\0" }
	, { SYBERPNULL,         EXPROGRAM,	"value parameter for dbrpcparam can be NULL, only if the datalen parameter is 0\0" }
	, { SYBERPTXTIM,        EXPROGRAM,	"RPC parameters cannot be of type text or image\0" }
	, { SYBERPUL,           EXPROGRAM,	"When passing a SYBINTN, SYBDATETIMN, SYBMONEYN, or SYBFLTN parameter via "
						"dbrpcparam, it is necessary to specify the parameter's maximum or actual length so "
						"that DB-Library can recognize it as a SYINT1, SYBINT2, SYBINT4, SYBMONEY, SYBMONEY4, "
						"and so on\0" }
	, { SYBERTCC,           EXPROGRAM,	"dbreadtext may not be used to receive the results of a query that contains a "
						"COMPUTE clause\0" }
	, { SYBERTSC,           EXPROGRAM,	"dbreadtext may be used only to receive the results of a query that contains a "
						"single result column\0" }
	, { SYBERXID,          EXNONFATAL,	"The Server did not recognize our distributed-transaction ID\0" }
	, { SYBESECURE,         EXPROGRAM,	"Secure SQL Server function not supported in this version\0" }
	, { SYBESEFA,           EXPROGRAM,	"DBSETNOTIFS cannot be called if connections are present\0" }
	, { SYBESEOF,              EXCOMM,	"Unexpected EOF from the server\0" }
	, { SYBESFOV,           EXPROGRAM,	"International Release: dbsafestr overflowed its destination buffer\0" }
	, { SYBESMSG,            EXSERVER,	"General Adaptive Server error: Check messages from the server\0" }
	, { SYBESOCK,              EXCOMM,	"Unable to open socket\0" }
	, { SYBESPID,           EXPROGRAM,	"Called dbspid with a NULL dbproc\0" }
	, { SYBESYNC,              EXCOMM,	"Read attempted while out of synchronization with Adaptive Server\0" }
	, { SYBETEXS,              EXINFO,	"Called dbmoretext with a bad size parameter\0" }
	, { SYBETIME,              EXTIME,	"Adaptive Server connection timed out\0" }
	, { SYBETMCF,           EXPROGRAM,	"Attempt to install too many custom formats via dbfmtinstall\0" }
	, { SYBETMTD,           EXPROGRAM,	"Attempt to send too much TEXT data via the dbmoretext call\0" }
	, { SYBETPAR,           EXPROGRAM,	"No SYBTEXT or SYBIMAGE parameters were defined\0" }
	, { SYBETPTN,              EXUSER,	"Syntax error: only two periods are permitted in table names\0" }
	, { SYBETRAC,              EXINFO,	"Attempted to turn off a trace flag that was not on\0" }
	, { SYBETRAN,              EXINFO,	"DBPROCESS is being used for another transaction\0" }
	, { SYBETRAS,              EXINFO,	"DB-Library internal error - trace structure not found\0" }
	, { SYBETRSN,              EXINFO,	"Bad numbytes parameter passed to dbtracestring\0" }
	, { SYBETSIT,              EXINFO,	"Attempt to call dbtsput with an invalid timestamp\0" }
	, { SYBETTS,               EXUSER,	"The table which bulk copy is attempting to copy to a host file is shorter than the "
						"number of rows which bulk copy was instructed to skip\0" }
	, { SYBETYPE,              EXINFO,	"Invalid argument type given to Hyper/DB-Library\0" }
	, { SYBEUCPT,              EXUSER,	"Unrecognized custom-format parameter-type encountered in dbstrbuild\0" }
	, { SYBEUCRR,       EXCONSISTENCY,	"Internal software error: Unknown connection result reported by dbpasswd\0" }
	, { SYBEUDTY,       EXCONSISTENCY,	"Unknown datatype encountered\0" }
	, { SYBEUFDS,              EXUSER,	"Unrecognized format encountered in dbstrbuild\0" }
	, { SYBEUFDT,       EXCONSISTENCY,	"Unknown fixed-length datatype encountered\0" }
	, { SYBEUHST,              EXUSER,	"Unknown host machine name\0" }
	, { SYBEUMSG,              EXCOMM,	"Unknown message-id in MSG datastream\0" }
	, { SYBEUNAM,             EXFATAL,	"Unable to get current user name from operating system\0" }
	, { SYBEUNOP,          EXNONFATAL,	"Unknown option passed to dbsetopt\0" }
	, { SYBEUNT,               EXUSER,	"Unknown network type found in interface file\0" }
	, { SYBEURCI,          EXRESOURCE,	"International Release: Unable to read copyright information from the DB-Library "
						"localization file\0" }
	, { SYBEUREI,          EXRESOURCE,	"International Release: Unable to read error information from the DB-Library "
						"localization file\0" }
	, { SYBEUREM,          EXRESOURCE,	"International Release: Unable to read error mnemonic from the DB-Library "
						"localization file\0" }
	, { SYBEURES,          EXRESOURCE,	"International Release: Unable to read error string from the DB-Library "
						"localization file. 401 Error severities\0" }
	, { SYBEURMI,          EXRESOURCE,	"International Release: Unable to read money-format information from the DB-Library "
						"localization file\0" }
	, { SYBEUSCT,              EXCOMM,	"Unable to set communications timer\0" }
	, { SYBEUTDS,              EXCOMM,	"Unrecognized TDS version received from the server\0" }
	, { SYBEUVBF,           EXPROGRAM,	"Attempt to read an unknown version of bcp format file\0" }
	, { SYBEUVDT,       EXCONSISTENCY,	"Unknown variable-length datatype encountered\0" }
	, { SYBEVDPT,              EXUSER,	"For bulk copy, all variable-length data must have either a length-prefix or a "
						"terminator specified\0" }
	, { SYBEWAID,       EXCONSISTENCY,	"DB-Library internal error: ALTFMT following ALTNAME has wrong id\0" }
	, { SYBEWRIT,              EXCOMM,	"Write to the server failed\0" }
	, { SYBEXOCI,          EXNONFATAL,	"International Release: A character-set translation overflowed its destination "
						"buffer while using bcp to copy data from a host-file to the server\0" }
	, { SYBEXTDN,           EXPROGRAM,	"Warning: the xlt_todisp parameter to dbfree_xlate was NULL. The space associated "
						"with the xlt_tosrv parameter has been freed\0" }
	, { SYBEXTN,            EXPROGRAM,	"The xlt_tosrv and xlt_todisp parameters to dbfree_xlate were NULL\0" }
	, { SYBEXTSN,           EXPROGRAM,	"Warning: the xlt_tosrv parameter to dbfree_xlate was NULL. The space associated "
						"with the xlt_todisp parameter has been freed\0" }
	, { SYBEZTXT,              EXINFO,	"Attempt to send zero length TEXT or IMAGE to dataserver via dbwritetext\0" }
	, { SYBECOLSIZE,           EXINFO,      "Invalid column information structure size\0" }
	};

/**  \internal
 * \ingroup dblib_internal
 * \brief Call client-installed error handler
 * 
 * \param dbproc contains all information needed by db-lib to manage communications with the server.
 * \param msgno	identifies the error message to be passed to the client's handler.
 * \param errnum identifies the OS error (errno), if any.  Use 0 if not applicable.  
 * \returns the handler's return code, subject to correction and adjustment for vendor style:
 * 	- INT_CANCEL	The db-lib function that encountered the error will return FAIL.  
 * 	- INT_TIMEOUT	The db-lib function will cancel the operation and return FAIL.  \a dbproc remains useable.  
 * 	- INT_CONTINUE	The db-lib function will retry the operation.  
 * \remarks 
 *	The client-installed handler may also return INT_EXIT.  If Sybase semantics are used, this function notifies
 * 	the user and calls exit(3).  If Microsoft semantics are used, this function returns INT_CANCEL.  
 *
 *	If the client-installed handler returns something other than these four INT_* values, or returns timeout-related
 *	value for anything but SYBETIME, it's treated here as INT_EXIT (see above).  
 *
 * Instead of sprinkling error text all over db-lib, we consolidate it here, 
 * where it can be translated (one day), and where it can be mapped to the TDS error number.  
 * The libraries don't use consistent error numbers or messages, so when libtds has to emit 
 * an error message, it can't include the text.  It can pass its error number to a client-library
 * function, which will interpret it, add the text, call the application's installed handler
 * (if any) and return the handler's return code back to the caller.  
 * 
 * The call stack may look something like this:
 *
 * -#	application
 * -#		db-lib function (encounters error)
 * -#		dbperror
 * -#	error handler (installed by application)
 *
 * The error handling in this case is unambiguous: the caller invokes this function, the client's handler returns its 
 * instruction, which the caller receives.  Quite often the caller will get INT_CANCEL, in which case it should put its 
 * house in order and return FAIL.  
 *
 * The call stack may otherwise look something like this:
 *			
 * -#	application
 * -#		db-lib function
 * -#			libtds function (encounters error)
 * -#		_dblib_handle_err_message
 * -#		dbperror
 * -# 	error handler (installed by application)
 *
 * Because different client libraries specify their handler semantics differently, 
 * and because libtds doesn't know which client library is in charge of any given connection, it cannot interpret the 
 * raw return code from a db-lib error handler.  For these reasons, 
 * libtds calls _dblib_handle_err_message, which translates between libtds and db-lib semantics.  
 * \sa dberrhandle(), _dblib_handle_err_message().
 */
int
dbperror (DBPROCESS *dbproc, DBINT msgno, long errnum, ...)
{
	static const char int_exit_text[] = "FreeTDS: db-lib: exiting because client error handler returned %s for msgno %d\n";
	static const char int_invalid_text[] = "%s (%d) received from client-installed error handler for nontimeout for error %d."
					       "  Treating as INT_EXIT\n";
	static const DBLIB_ERROR_MESSAGE default_message = { 0, EXCONSISTENCY, "unrecognized msgno" };
	DBLIB_ERROR_MESSAGE constructed_message = { 0, EXCONSISTENCY, NULL };
	const DBLIB_ERROR_MESSAGE *msg = &default_message;
	
	int i, rc = INT_CANCEL;
	const char *os_msgtext = strerror(errnum), *rc_name = "logic error";
	char rc_buf[16];

	tdsdump_log(TDS_DBG_FUNC, "dbperror(%p, %d, %ld)\n", dbproc, msgno, errnum);	/* dbproc can be NULL */

#ifdef _WIN32
	/*
	 * Unfortunately MingW uses the "old" msvcrt.dll (Visual C++ 2005 uses
	 * a newer version) which does not set errno when allocation functions
	 * cannot allocate memory, so we do it for them.
	 */
	if (msgno == SYBEMEM)
		errnum = ENOMEM;
#endif

	if (os_msgtext == NULL)
		os_msgtext = "no OS error";
	
	assert(_dblib_err_handler != NULL);	/* always installed by dbinit() or dberrhandle() */

	/* look up the error message */
	for (i=0; i < TDS_VECTOR_SIZE(dblib_error_messages); i++ ) {
		if (dblib_error_messages[i].msgno == msgno) {

			/* 
			 * See if the message has placeholders.  If so, build a message string on the heap.  
			 * The presence of placeholders is indicated by the existence of a "string after the string", 
			 * i.e., a format string (for dbstrbuild) after a null "terminator" in the message. 
			 * On error -- can't allocate, can't build the string -- give up and call the client handler anyway. 
			 */
			const char * ptext = dblib_error_messages[i].msgtext;
			const char * pformats = ptext + strlen(ptext) + 1;
			msg = &dblib_error_messages[i];
			assert(*(pformats - 1) == '\0'); 
			if(*pformats != '\0') {
				va_list ap;
				int result_len, len = 2 * (int)strlen(ptext);
				char * buffer = tds_new0(char, len);

				if (buffer == NULL)
					break;
				va_start(ap, errnum);
				rc = tds_vstrbuild(buffer, len, &result_len, ptext, TDS_NULLTERM, pformats, TDS_NULLTERM, ap);
				buffer[result_len] = '\0';
				va_end(ap);
				if (TDS_FAILED(rc)) {
					free(buffer);
					break;
				}
				constructed_message.msgtext = buffer;
				constructed_message.severity = msg->severity;
				msg = &constructed_message;
			}
			break;
		}
	}

	if (dbproc && dbproc->tds_socket && dbproc->tds_socket->login) {
		DSTR server_name_dstr = dbproc->tds_socket->login->server_name;
		if (!tds_dstr_isempty(&server_name_dstr)) {
			char * buffer = NULL;
			if (asprintf(&buffer, "%s (%s)", msg->msgtext,
			             tds_dstr_cstr(&server_name_dstr)) >= 0) {
				free((char*) constructed_message.msgtext);
				constructed_message.msgtext = buffer;
				constructed_message.severity = msg->severity;
				msg = &constructed_message;
			}
		}
	}

	tdsdump_log(TDS_DBG_FUNC, "dbperror: Calling dblib_err_handler with msgno = %d; msg->msgtext = \"%s\"\n",
		msgno, msg->msgtext);

	/* call the error handler */
	rc = (*_dblib_err_handler)(dbproc, msg->severity, msgno, errnum, (char*) msg->msgtext, (char*) os_msgtext);
	switch (rc) {
	case INT_EXIT:
		rc_name = "INT_EXIT";	
		break;
	case INT_CONTINUE:	
		rc_name = "INT_CONTINUE";
		break;
	case INT_CANCEL:
		rc_name = "INT_CANCEL";
		break;
	case INT_TIMEOUT:
		rc_name = "INT_TIMEOUT";
		break;
	default:
		rc_name = "invalid";
		break;
	}
	tdsdump_log(TDS_DBG_FUNC, "dbperror: dblib_err_handler for msgno = %d; msg->msgtext = \"%s\" -- returns %d (%s)\n",
		msgno, msg->msgtext, rc, rc_name);

	/* we're done with the dynamic string now. */
	free((char*) constructed_message.msgtext);
	
      	/* Timeout return codes are errors for non-timeout conditions. */
	if (msgno != SYBETIME) {
		switch (rc) {
		case INT_CONTINUE:
			tdsdump_log(TDS_DBG_SEVERE, int_invalid_text, "INT_CONTINUE", rc, msgno);
			rc = INT_EXIT;
			break;
		case INT_TIMEOUT:
			tdsdump_log(TDS_DBG_SEVERE, int_invalid_text, "INT_TIMEOUT", rc, msgno);
			rc = INT_EXIT;
			break;
		default:
			break;
		}
	}

	/* 
	 * Sybase exits on INT_EXIT; Microsoft converts to INT_CANCEL.
	 * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dblibc/dbc_pdc04c_6v39.asp
	 */
	switch (rc) {
	case INT_CONTINUE:
		/* Microsoft does not define INT_TIMEOUT.  Instead, two consecutive INT_CONTINUEs yield INT_CANCEL. */
		if (dbproc && dbproc->msdblib && ++dbproc->ntimeouts >=2) {
			dbproc->ntimeouts = 0;
			rc = INT_CANCEL;
		}	/* fall through */
	case INT_CANCEL:
	case INT_TIMEOUT:
		return rc;	/* normal case */
		break;
	default:
		sprintf(rc_buf, "%d", rc);
		rc_name = rc_buf;
		tdsdump_log(TDS_DBG_SEVERE, int_invalid_text, "Invalid return code", rc, msgno);
		/* fall through */
	case INT_EXIT:
		if (dbproc && dbproc->msdblib) {
			/* Microsoft behavior */
			return INT_CANCEL;
		}
		fprintf(stderr, int_exit_text, rc_name, msgno);
		tdsdump_log(TDS_DBG_SEVERE, int_exit_text, rc_name, msgno);
		break;
	}
	exit(EXIT_FAILURE);
	return rc; /* not reached */
}

