/*
 * dbodbc.c  -  ODBC database routines
 *
 * Copyright (C) 2006-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: dbodbc.c,v 1.5 2007/01/06 18:31:38 gkminix Exp $
 */

#include <common.h>
#include <nblib.h>

#ifdef HAVE_ODBC

#include <sql.h>
#include <sqlext.h>
#include "privlib.h"
#include "privdb.h"



/*
 * Check that we have an ODBC library which supports at least version 3.x
 */
#if !defined(ODBCVER) || ODBCVER < 0x0300
# error Invalid version of ODBC library
#endif



/*
 * Special record ID values
 */
#define CURID_BOF	(unsigned long)(-1)	/* beginning of file */
#define CURID_EOF	(unsigned long)(-2)	/* end of file */

#define curid_valid(id)	(id != CURID_BOF && id != CURID_EOF)



/*
 * Direction commands
 */
#define DIR_FIRST		0	/* get first record */
#define DIR_LAST		1	/* get last record */
#define DIR_NEXT		2	/* get next record */
#define DIR_PREV		3	/* get previous record */
#define DIR_FIRST_REC		4	/* get first record with record name */
#define DIR_NEXT_REC		5	/* get next record with record name */



/*
 * Types of database items
 */
#define DBTYPE_NONE	0
#define DBTYPE_STRING	1
#define DBTYPE_INTEGER	2
#define DBTYPE_BOOLEAN	3



/*
 * Definition of table column names
 */
static char *colnames[] = {
	"COLUMN_NAME",
	"DATA_TYPE",
	"id",
	"sysname",
	"sysid",
	"name",
	"type",
	"strval",
	"intval",
	"boolval",
	"description"
};

/* These are the index values into this string array */
#define STR_INVALID		-1
#define STR_COLUMN_NAME		 0
#define STR_DATA_TYPE		 1
#define STR_ID			 2
#define STR_SYSNAME		 3
#define STR_SYSID		 4
#define STR_PARAMNAME		 5
#define STR_PARAMTYPE		 6
#define STR_PARAMSTRVAL		 7
#define STR_PARAMINTVAL		 8
#define STR_PARAMBOOLVAL	 9
#define STR_DESCRIPTION		10
#define STR_MAX_ID		10




/*
 * Layout of netboot tables
 */
struct tablelayout {
	int     nameid;			/* ID of column name */
	int     sqltype;		/* SQL data type */
};


/* Systems table */
static struct tablelayout syslayout[] = {
	{ STR_ID,		SQL_INTEGER },
	{ STR_SYSNAME,		SQL_VARCHAR },
	{ STR_DESCRIPTION,	SQL_VARCHAR },
	{ STR_INVALID,		SQL_UNKNOWN_TYPE }
};


/* Parameter table */
static struct tablelayout paramlayout[] = {
	{ STR_SYSID,		SQL_INTEGER },
	{ STR_PARAMNAME,	SQL_VARCHAR },
	{ STR_PARAMTYPE,	SQL_SMALLINT },
	{ STR_PARAMSTRVAL,	SQL_VARCHAR },
	{ STR_PARAMINTVAL,	SQL_INTEGER },
	{ STR_PARAMBOOLVAL,	SQL_TINYINT },
	{ STR_INVALID,		SQL_UNKNOWN_TYPE }
};




/*
 * Definitions of all columns in a table
 */
struct colinfo {
	char    name[128];		/* name of column */
	short   type;			/* type of column data */
	long    len;			/* length of data in buffer */
	size_t  size;			/* size of column data buffer */
	union {
	  char          *strval;	/* string result */
	  long           intval;	/* integer result */
	  unsigned long  uintval;	/* unsigned integer result */
	  short          shortval;	/* short result */
	  unsigned short ushortval;	/* unsigned short result */
	  unsigned char  boolval;	/* boolean result */
	} val;
};



/*
 * Result from executing an SQL statement
 */
struct sqlresult {
	int             numcols;	/* number of data columns */
	char           *buf;		/* buffer holding string data */
	size_t          bufsize;	/* buffer size */
	struct colinfo *info;		/* data info */
};



/*
 * Structure holding execution information
 */
struct sqlexec {
	SQLHSTMT          stmt;		/* SQL statement handle */
	char             *cmd;		/* Latest SQL command string */
	struct sqlresult *result;	/* execution result */
};



/*
 * Structure holding information about an open database
 */
struct dbodbc {
	SQLHDBC           conn;		/* SQL connection handle */
	int               flags;	/* open flags */
	int               locked;	/* locked flag */
	unsigned long     curid;	/* current record ID */
	unsigned long     mark;		/* record mark */
	struct dbconfig  *config;	/* database configuration */
};




/*
 * Local variables
 */
static SQLHENV sqlenv = SQL_NULL_HENV ;	/* database environment */
static int opencount = 0;		/* count of open handles */




/*
 * Check for an error return, and call the error handler. This function
 * returns FALSE, if an error occurred.
 */
static int checkerr __F((errnum, msg), int errnum AND const char *msg)
{
  if (errnum == SQL_INVALID_HANDLE) {
	nblib_db_error("invalid database handle", DBERR_CLASS_FATAL);
	return(FALSE);
  } else if (errnum == SQL_ERROR) {
	nblib_db_error(msg, DBERR_CLASS_ERROR);
	return(FALSE);
  }
  return(TRUE);
}



/*
 * Allocate data set info
 */
static struct sqlresult *allocresult __F((stmt), SQLHSTMT stmt)
{
  struct sqlresult *ret;
  struct colinfo *info = NULL;
  size_t size = 0;
  unsigned i;
  short colnum, namelen;
  int errnum, numval;
  char *cp, *buf = NULL;

  /* Determine the number of columns in the result set */
  errnum = SQLNumResultCols(stmt, &colnum);
  if (!checkerr(errnum, "unable to determine number of columns"))
	return(NULL);

  /* Check if we have any data at all */
  if (colnum < 0 || colnum > 255) {
	nblib_db_error("invalid number of columns", DBERR_CLASS_ERROR);
	return(NULL);
  } else if (colnum == 0)
	goto resultend;

  /* Allocate array of records with info for each column */
  info = (struct colinfo *)nbmalloc(colnum * sizeof(struct colinfo));
  size = 0;
  for (i = 0; i < colnum; i++) {

	/* Get column name */
	errnum = SQLColAttribute(stmt, (i + 1), SQL_DESC_NAME,
						(SQLPOINTER)(info[i].name),
						sizeof(info[i].name),
						&namelen, NULL);
	if (!checkerr(errnum, "unable to determine column name")) {
		free(info);
		return(NULL);
	}

	/* Determine type of column */
	numval = 0;
	errnum = SQLColAttributes(stmt, (i + 1), SQL_DESC_TYPE,
					NULL, 0, NULL, (SQLPOINTER)&numval);
	if (!checkerr(errnum, "unable to determine column data type")) {
		free(info);
		return(NULL);
	}

	/* Convert SQL data types into C data types */
	switch (numval) {
		case SQL_INTEGER:
			info[i].type = SQL_C_LONG;
			break;
		case SQL_SMALLINT:
			info[i].type = SQL_C_SHORT;
			break;
		case SQL_VARCHAR:
			info[i].type = SQL_C_CHAR;
			break;
		case SQL_TINYINT:
			info[i].type = SQL_C_TINYINT;
			break;
		default:
			info[i].type = SQL_TYPE_NULL;
			break;
	}

	/* Determine signedness */
	if (info[i].type == SQL_C_LONG || info[i].type == SQL_C_SHORT) {
		numval = 0;
		errnum = SQLColAttributes(stmt, (i + 1), SQL_DESC_UNSIGNED,
					NULL, 0, NULL, (SQLPOINTER)&numval);
		if (!checkerr(errnum, "unable to determine signedness")) {
			free(info);
			return(NULL);
		}
		if (numval == SQL_TRUE)
			info[i].type += SQL_UNSIGNED_OFFSET;
	}

	/* Determine the size of the column data buffer */
	if (info[i].type == SQL_C_CHAR) {
		errnum = SQLColAttributes(stmt, (i + 1), SQL_DESC_DISPLAY_SIZE,
				NULL, 0, NULL, (SQLPOINTER)&(info[i].size));
		if (!checkerr(errnum, "unable to determine column data size")) {
			free(info);
			return(NULL);
		}
		size += info[i].size + 1;
	}
  }

  /* Assign data buffer to each character column description */
  buf = cp = (char *)nbmalloc(size + 1);
  for (i = 0; i < colnum; i++)
	if (info[i].type == SQL_C_CHAR) {
		info[i].val.strval = cp;
		cp += info[i].size;
	}

  /* Bind all columns to current statement */
  for (i = 0; i < colnum; i++) {
	if (info[i].type == SQL_C_CHAR)
		(void)SQLBindCol(stmt, (i + 1), SQL_C_CHAR,
					(SQLPOINTER)(info[i].val.strval),
					(SQLLEN)(info[i].size),
					(SQLLEN *)&(info[i].len));
	else if (info[i].type != SQL_TYPE_NULL)
		(void)SQLBindCol(stmt, (i + 1), info[i].type,
					(SQLPOINTER)&(info[i].val.intval),
					0,
					(SQLLEN *)&(info[i].len));
  }

  /* Allocate new data structure */
resultend:
  ret = (struct sqlresult *)nbmalloc(sizeof(struct sqlresult));
  ret->numcols = colnum;
  ret->info = info;
  ret->buf = buf;
  ret->bufsize = size;
  return(ret);
}



/*
 * Delete data set info
 */
static void freeresult __F((res), struct sqlresult *res)
{
  if (res != NULL) {
	if (res->buf != NULL)
		free(res->buf);
	if (res->info != NULL)
		free(res->info);
	free(res);
  }
}



/*
 * Find a column in a result set
 */
static struct colinfo *findcolumn __F((res, id),
					struct sqlresult *res AND
					int id)
{
  struct colinfo *ret;
  int i;

  assert(id >= 0 && id <= STR_MAX_ID);
  assert(res->numcols > 0 && res->info != NULL);
  for (i = 0; i < res->numcols; i++) {
	ret = &(res->info[i]);
	if (!strcmp(ret->name, colnames[id]))
		return(ret);
  }
  return(NULL);
}



/*
 * Read a string value from SQL result
 */
static char __attribute__((unused)) *getstrresult __F((res, id),
						struct sqlresult *res AND
						int id)
{
  struct colinfo *info;
  size_t size;
  char *cp;

  /* Find column info */
  if ((info = findcolumn(res, id)) == NULL)
	return(NULL);

  /* Check the type */
  if (info->type != SQL_C_CHAR)
	return(NULL);

  /* Check if we have NULL data */
  if (info->len == SQL_NULL_DATA) {
	cp = (char *)nbmalloc(1);
	*cp = '\0';
	return(cp);
  }

  /* Copy the string out of the result buffer */
  size = (size_t)(info->len);
  if (size > info->size)
	size = info->size;
  cp = (char *)nbmalloc(size + 1);
  strncpy(cp, info->val.strval, size);
  cp[size] = '\0';

  /* Return pointer to column value */
  return(cp);
}



/*
 * Read a long value from SQL result
 */
static long __attribute__((unused)) *getlongresult __F((res, id),
						struct sqlresult *res AND
						int id)
{
  struct colinfo *info;

  /* Find column info */
  if ((info = findcolumn(res, id)) == NULL)
	return(NULL);

  /* Check the type */
  if (info->type != SQL_C_LONG)
	return(NULL);

  /* Check if we have NULL data */
  if (info->len == SQL_NULL_DATA)
	return(NULL);

  /* Return pointer to column value */
  return(&(info->val.intval));
}



/*
 * Read a unsigned long value from SQL result
 */
static unsigned long __attribute__((unused)) *getulongresult __F((res, id),
						struct sqlresult *res AND
						int id)
{
  struct colinfo *info;

  /* Find column info */
  if ((info = findcolumn(res, id)) == NULL)
	return(NULL);

  /* Check the type */
  if (info->type != SQL_C_ULONG)
	return(NULL);

  /* Check if we have NULL data */
  if (info->len == SQL_NULL_DATA)
	return(NULL);

  /* Return pointer to column value */
  return(&(info->val.uintval));
}



/*
 * Read a short value from SQL result
 */
static short __attribute__((unused)) *getshortresult __F((res, id),
						struct sqlresult *res AND
						int id)
{
  struct colinfo *info;

  /* Find column info */
  if ((info = findcolumn(res, id)) == NULL)
	return(NULL);

  /* Check the type */
  if (info->type != SQL_C_SHORT)
	return(NULL);

  /* Check if we have NULL data */
  if (info->len == SQL_NULL_DATA)
	return(NULL);

  /* Return pointer to column value */
  return(&(info->val.shortval));
}



/*
 * Read a short value from SQL result
 */
static unsigned short __attribute__((unused)) *getushortresult __F((res, id),
						struct sqlresult *res AND
						int id)
{
  struct colinfo *info;

  /* Find column info */
  if ((info = findcolumn(res, id)) == NULL)
	return(NULL);

  /* Check the type */
  if (info->type != SQL_C_USHORT)
	return(NULL);

  /* Check if we have NULL data */
  if (info->len == SQL_NULL_DATA)
	return(NULL);

  /* Return pointer to column value */
  return(&(info->val.ushortval));
}



/*
 * Read a boolean value from SQL result
 */
static unsigned char __attribute__((unused)) *getbitresult __F((res, id),
						struct sqlresult *res AND
						int id)
{
  struct colinfo *info;

  /* Find column info */
  if ((info = findcolumn(res, id)) == NULL)
	return(NULL);

  /* Check the type */
  if (info->type != SQL_C_TINYINT)
	return(NULL);

  /* Check if we have NULL data */
  if (info->len == SQL_NULL_DATA)
	return(NULL);

  /* Return pointer to column value */
  return(&(info->val.boolval));
}



/*
 * Execute an SQL command
 * If the result argument pointer is NULL, no result structure will be
 * created
 */
static struct sqlexec *execute __F((db, cmd, noresult),
					struct dbodbc *db AND
					const char *cmd)
{
  struct sqlexec *exec;
  int errnum;

  /* Create a new exec structure */
  assert(cmd != NULL);
  exec = (struct sqlexec *)nbmalloc(sizeof(struct sqlexec));
  copystr(&(exec->cmd), cmd);

  /* Get us a new execution handle */
  errnum = SQLAllocHandle(SQL_HANDLE_STMT, db->conn, &(exec->stmt));
  if (!checkerr(errnum, "unable to create database statement handle"))
	goto execerr1;

  /* Execute the statement */
  errnum = SQLExecDirect(exec->stmt, (SQLCHAR *)exec->cmd, SQL_NTS);
  if (!checkerr(errnum, "unable to execute SQL command"))
	goto execerr2;
  if (errnum == SQL_NEED_DATA || errnum == SQL_NO_DATA) {
	nblib_db_error("invalid SQL command", DBERR_CLASS_ERROR);
	goto execerr2;
  }

  /* Assign a result structure to the SQL result set */
  exec->result = allocresult(exec->stmt);
  if (exec->result == NULL) {
execerr2:
	(void)SQLFreeHandle(SQL_HANDLE_STMT, exec->stmt);
execerr1:
	free(exec->cmd);
	free(exec);
	return(NULL);
  }

  /* Return the result code from execute statement */
  return(exec);
}



/*
 * End command execution
 */
static void endexec __F((exec), struct sqlexec *exec)
{
  if (exec != NULL) {
	(void)SQLFreeHandle(SQL_HANDLE_STMT, exec->stmt);
	freeresult(exec->result);
	free(exec->cmd);
	free(exec);
  }
}



/*
 * Get next record ID
 */
static int getnext __F((db, recname, cmd),
				struct dbodbc *db AND
				const char *recname AND
				int cmd)
{
  struct sqlexec *exec;
  char *sql, *name;
  int errnum, ret = FALSE;

  /* Check for proper direction command */
  if (db->curid == CURID_BOF) {
	if (cmd == DIR_NEXT_REC)
		cmd = DIR_FIRST_REC;
	else if (cmd == DIR_NEXT)
		cmd = DIR_FIRST;
	else if (cmd == DIR_PREV)
		return(FALSE);
  } else if (db->curid == CURID_EOF) {
	if (cmd == DIR_NEXT_REC || cmd == DIR_NEXT)
		return(FALSE);
	else if (cmd == DIR_PREV)
		cmd = DIR_LAST;
  }

  /* Replace any wildcard character in the record name */
  if (recname != NULL && *recname) {
	char *cp1;
	const char *cp2;

	name = (char *)nbmalloc(strlen(recname) * 2 + 1);
	cp1 = name;
	cp2 = recname;
	while (*cp2) {
		switch (*cp2) {
			case '%':
			case '_':
				*cp1++ = '\\';
				*cp1++ = *cp2;
				break;
			case DBMATCH_MULTI:
				*cp1++ = '%';
				break;
			case DBMATCH_SINGLE:
				*cp1++ = '_';
				break;
			default:
				*cp1++ = *cp2;
				break;
		}
		cp2++;
	}
	*cp1 = '\0';
  } else {
	/* If no record name given, adjust the command code */
	name = NULL;
	if (cmd == DIR_FIRST_REC)
		cmd = DIR_FIRST;
	else if (cmd == DIR_NEXT_REC)
		cmd = DIR_NEXT;
  }

  /* Create the proper SQL statement */
  {
	char *systable, *str;
	size_t size;

	systable = db->config->config.sql.systable;
	size = strlen(colnames[STR_ID]) * 2 + strlen(systable);
	switch (cmd) {
		default:
		case DIR_FIRST:
			str = "SELECT MIN(%s) AS %s FROM %s";
			break;
		case DIR_LAST:
			str = "SELECT MAX(%s) AS %s FROM %s";
			break;
		case DIR_NEXT:
			str = "SELECT MIN(%s) AS %s FROM %s WHERE %s > %lu";
			size += strlen(colnames[STR_ID]) + 20;
			break;
		case DIR_PREV:
			str = "SELECT MAX(%s) AS %s FROM %s WHERE %s < %lu";
			size += strlen(colnames[STR_ID]) + 20;
			break;
		case DIR_FIRST_REC:
			str = "SELECT MIN(%s) AS %s FROM %s WHERE %s LIKE '%s'";
			size += strlen(colnames[STR_SYSNAME]) + strlen(name);
			break;
		case DIR_NEXT_REC:
			str = "SELECT MIN(%s) AS %s FROM %s WHERE %s LIKE '%s' AND %s > %lu";
			size += strlen(colnames[STR_SYSNAME]) + strlen(name) +
						strlen(colnames[STR_ID]) + 20;
			break;
	}
	sql = (char *)nbmalloc(strlen(str) + size + 1);
	switch (cmd) {
		case DIR_FIRST:
		case DIR_LAST:
			sprintf(sql, str, colnames[STR_ID],
							colnames[STR_ID],
							systable);
			break;
		case DIR_NEXT:
		case DIR_PREV:
			sprintf(sql, str, colnames[STR_ID],
							colnames[STR_ID],
							systable,
							colnames[STR_ID],
							db->curid);
			break;
		case DIR_FIRST_REC:
			sprintf(sql, str, colnames[STR_ID],
							colnames[STR_ID],
							systable,
							colnames[STR_SYSNAME],
							name);
			break;
		case DIR_NEXT_REC:
			sprintf(sql, str, colnames[STR_ID],
							colnames[STR_ID],
							systable,
							colnames[STR_SYSNAME],
							name,
							colnames[STR_ID],
							db->curid);
			break;
	}
  }
  if (name != NULL)
	free(name);

  /* Execute SELECT command */
  if ((exec = execute(db, sql)) == NULL)
	goto nextend;

  /* Fetch result from database */
  errnum = SQLFetch(exec->stmt);
  if (checkerr(errnum, "unable to fetch next record ID")) {
	if (errnum != SQL_NO_DATA) {
		unsigned long *lp;

		lp = getulongresult(exec->result, STR_ID);
		if (lp != NULL) {
			db->curid = *lp;
			ret = TRUE;
		}
	}
	if (!ret) {
		if (cmd == DIR_FIRST || cmd == DIR_FIRST_REC || cmd == DIR_PREV)
			db->curid = CURID_BOF;
		else if (cmd == DIR_LAST || cmd == DIR_NEXT || cmd == DIR_NEXT_REC)
			db->curid = CURID_EOF;
	}
  }
  endexec(exec);

nextend:
  free(sql);
  return(ret);
}



/*
 * Lock or unlock the database
 */
static int lockdb __F((db, unlock), struct dbodbc *db AND int unlock)
{
  struct sqlexec *exec;
  char *sql, *str, *write;
  char *systable = db->config->config.sql.systable;
  char *paramtable = db->config->config.sql.paramtable;
  size_t size;
  int ret = FALSE;

  /* Check if we have anything to do */
  if ((db->locked && !unlock) || (!(db->locked) && unlock))
	return(TRUE);

  /* Create SQL string */
  write =((db->flags & DBFLAGS_RO) != 0 ? "READ" : "WRITE");
  if (unlock) {
	str = "UNLOCK TABLES";
	size = 0;
  } else {
	str = "LOCK TABLES %s %s, %s %s";
	size = strlen(systable) + strlen(paramtable) + 2 * strlen(write);
  }
  sql = (char *)nbmalloc(strlen(str) + size + 1);
  sprintf(sql, str, systable, write, paramtable, write);

  /* Execute SQL statement */
  if ((exec = execute(db, sql)) != NULL) {
	endexec(exec);
	db->locked = !unlock;
	ret = TRUE;
  }
  free(sql);
  return(ret);
}



/*
 * Read name of current record
 */
static char *getname __F((db, id), struct dbodbc *db)
{
  struct sqlexec *exec;
  char *str, *sql, *ret = NULL;
  char *systable = db->config->config.sql.systable;
  int errnum;

  /* Check if we have a valid cursor position */
  if (!curid_valid(db->curid))
	return(NULL);

  /* Create SQL string */
  str = "SELECT %s FROM %s WHERE %s = %lu";
  sql = (char *)nbmalloc(strlen(str) + strlen(colnames[STR_SYSNAME]) +
			strlen(systable) + strlen(colnames[STR_ID]) + 21);
  sprintf(sql, str, colnames[STR_SYSNAME], systable, colnames[STR_ID],
								db->curid);

  /* Execute SQL statement */
  if ((exec = execute(db, sql)) == NULL)
	goto getnameend;

  /* Fetch result from database */
  errnum = SQLFetch(exec->stmt);
  if (checkerr(errnum, "unable to fetch current record name")) {
	if (errnum != SQL_NO_DATA)
		ret = getstrresult(exec->result, STR_SYSNAME);
	if (ret == NULL || !*ret) {
		/*
		 * This is definitely an error because every record ID has to have a
		 * name
		 */
		if (ret != NULL) {
			free(ret);
			ret = NULL;
		}
		nblib_db_error("no record name found", DBERR_CLASS_ERROR);
	}
  }
  endexec(exec);

getnameend:
  free(sql);
  return(ret);
}



/*
 * Delete current record
 */
static int delsysrec __F((db, paramsonly), struct dbodbc *db AND int paramsonly)
{
  struct sqlexec *exec;
  char *str, *sql;
  char *systable = db->config->config.sql.systable;
  char *paramtable = db->config->config.sql.paramtable;
  int ret = TRUE;

  /* Check if we have a valid cursor position */
  if (!curid_valid(db->curid))
	return(TRUE);

  /* Create SQL string for systems table */
  str = "DELETE FROM %s WHERE %s = %lu";
  if (!paramsonly) {
	sql = (char *)nbmalloc(strlen(str) + strlen(systable) +
					strlen(colnames[STR_ID]) + 21);
	sprintf(sql, str, systable, colnames[STR_ID], db->curid);
	if ((exec = execute(db, sql)) == NULL)
		ret = FALSE;
	else
		endexec(exec);
	free(sql);
  }

  /* Create SQL string for parameter table */
  sql = (char *)nbmalloc(strlen(str) + strlen(paramtable) +
					strlen(colnames[STR_SYSID]) + 21);
  sprintf(sql, str, paramtable, colnames[STR_SYSID], db->curid);
  if ((exec = execute(db, sql)) == NULL)
	ret = FALSE;
  else
	endexec(exec);
  free(sql);
  return(ret);
}



/*
 * Decode a database record. Returns NULL if at end of item list.
 */
static struct dbitem *decode __F((exec), struct sqlexec *exec)
{
  struct sqlresult *res = exec->result;
  struct dbitem *ip;
  int datavalid, errnum;
  unsigned short itemtype;
  char *name;
  union {
	char *cp;
	long *lp;
	unsigned short *sp;
	unsigned char *bp;
  } ptr;

  /* Fetch next data set */
  errnum = SQLFetch(exec->stmt);
  if (!checkerr(errnum, "unable to get parameter") || errnum == SQL_NO_DATA)
	return(NULL);

  /* Determine the parameter name */
  if ((name = getstrresult(res, STR_PARAMNAME)) == NULL || !*name) {
	nblib_db_error("invalid or missing item name", DBERR_CLASS_WARNING);
	copystr(&name, "<INVALID>");
  }

  /* Determine the parameter type */
  if ((ptr.sp = getushortresult(res, STR_PARAMTYPE)) == NULL) {
	nblib_db_error("invalid or missing item type", DBERR_CLASS_WARNING);
	itemtype = DBTYPE_NONE;
  } else
	itemtype = *ptr.sp;

  /* Create a new item structure */
  ip = (struct dbitem *)nbmalloc(sizeof(struct dbitem));
  ip->next = NULL;
  ip->name = name;

  /* Decode the item type and value */
  datavalid = FALSE;
  switch (itemtype) {
	case DBTYPE_NONE:
			ip->type = item_none;
			datavalid = TRUE;
			break;
	case DBTYPE_STRING:
			if ((ptr.cp = getstrresult(res, STR_PARAMSTRVAL)) == NULL)
				break;
			if (!*ptr.cp) {
				free(ptr.cp);
				ptr.cp = NULL;
			}
			ip->val.s = ptr.cp;
			ip->type = item_string;
			datavalid = TRUE;
			break;
	case DBTYPE_INTEGER:
			if ((ptr.lp = getlongresult(res, STR_PARAMINTVAL)) == NULL)
				break;
			ip->val.i = *ptr.lp;
			ip->type = item_integer;
			datavalid = TRUE;
			break;
	case DBTYPE_BOOLEAN:
			if ((ptr.bp = getbitresult(res, STR_PARAMBOOLVAL)) == NULL)
				break;
			ip->val.b = (*ptr.bp != '\0');
			ip->type = item_boolean;
			datavalid = TRUE;
			break;
	default:
			nblib_db_error("invalid item type",
						DBERR_CLASS_WARNING);
			ip->type = item_none;
			datavalid = TRUE;
			break;
  }
  if (!datavalid)
	nblib_db_error("invalid item data", DBERR_CLASS_WARNING);

  /* Return decoded item structure */
  return(ip);
}



/*
 * Read parameters for current record
 */
static struct dbitem *getparams __F((db), struct dbodbc *db)
{
  struct sqlexec *exec;
  struct dbitem *ip, *itemlist = NULL;
  char *str, *sql;
  char *paramtable = db->config->config.sql.paramtable;

  /* Check if we have a valid cursor position */
  if (!curid_valid(db->curid))
	return(NULL);

  /* Create SQL string */
  str = "SELECT * FROM %s WHERE %s = %lu";
  sql = (char *)nbmalloc(strlen(str) + strlen(paramtable) +
					strlen(colnames[STR_SYSID]) + 21);
  sprintf(sql, str, paramtable, colnames[STR_SYSID], db->curid);

  /* Execute SQL statement */
  if ((exec = execute(db, sql)) == NULL)
	goto getparamend;

  /* Fetch results from database */
  while ((ip = decode(exec)) != NULL) {
	ip->next = itemlist;
	itemlist = ip;
  }
  endexec(exec);

getparamend:
  free(sql);
  return(itemlist);
}



/*
 * Add a NULL value to another string, optionally with a comma.
 */
static char *addnull __F((target), char *target)
{
  size_t size;
  char *ret;

  size = 4;
  if (target != NULL)
	size += strlen(target) + 2;
  ret = (char *)nbmalloc(size + 1);
  if (target != NULL) {
	sprintf(ret, "%s, NULL", target);
	free(target);
  } else
	strcpy(ret, "NULL");
  return(ret);
}



/*
 * Add a boolean value to another string, optionally with a comma.
 */
static char *addbool __F((target, val), char *target AND int val)
{
  size_t size;
  char *ret, c;

  size = 1;
  if (target != NULL)
	size += strlen(target) + 2;
  ret = (char *)nbmalloc(size + 1);
  c = (val ? '1' : '0');
  if (target != NULL) {
	sprintf(ret, "%s, %c", target, c);
	free(target);
  } else {
	ret[0] = c;
	ret[1] = '\0';
  }
  return(ret);
}



/*
 * Add an integer number to another string, optionally with a comma.
 */
static char *addint __F((target, val), char *target AND long val)
{
  size_t size;
  char *ret;

  size = 21;
  if (target != NULL)
	size += strlen(target) + 2;
  ret = (char *)nbmalloc(size + 1);
  if (target != NULL) {
	sprintf(ret, "%s, %ld", target, val);
	free(target);
  } else
	sprintf(ret, "%ld", val);
  return(ret);
}



/*
 * Add an unsigned integer number to another string, optionally with a comma.
 */
static char *adduint __F((target, val), char *target AND unsigned long val)
{
  size_t size;
  char *ret;

  size = 21;
  if (target != NULL)
	size += strlen(target) + 2;
  ret = (char *)nbmalloc(size + 1);
  if (target != NULL) {
	sprintf(ret, "%s, %lu", target, val);
	free(target);
  } else
	sprintf(ret, "%lu", val);
  return(ret);
}



/*
 * Add a string to another string, optionally with a comma. It converts the string
 * according to SQL conversion rules.
 */
static char *addstr __F((target, str), char *target AND const char *str)
{
  size_t size;
  char *ret, *cp;

  /* Allocate enough space for the result string */
  size = (str != NULL ? strlen(str) * 2 + 2 : strlen("NULL"));
  if (target != NULL)
	size += strlen(target) + 2;
  ret = cp = (char *)nbmalloc(size + 1);
  if (target != NULL) {
	cp += sprintf(ret, "%s, ", target);
	free(target);
  }

  /* Insert NULL value if necessary */
  if (str == NULL) {
	str = "NULL";
	while (*str)
		*cp++ = *str++;
	*cp = '\0';
	return(ret);
  }

  /* Convert the source string using SQL conversion rules */
  *cp++ = '\'';
  while (*str) {
	switch (*str) {
		case '\'':
		case '"':
		case '\\':
			*cp++ = '\\';
			*cp++ = *str;
			break;
		case '\b':
			*cp++ = '\\';
			*cp++ = 'b';
			break;
		case '\n':
			*cp++ = '\\';
			*cp++ = 'n';
			break;
		case '\r':
			*cp++ = '\\';
			*cp++ = 'r';
			break;
		case '\t':
			*cp++ = '\\';
			*cp++ = 't';
			break;
		default:
			*cp++ = *str;
			break;
	}
	str++;
  }
  *cp++ = '\'';
  *cp = '\0';
  return(ret);
}



/*
 * Encode a parameter item into an SQL string
 */
static char *encode __F((db, item), struct dbodbc *db AND struct dbitem *item)
{
  char *paramtable = db->config->config.sql.paramtable;
  char *ret, *val, *str;

  /* Generate string with all values */
  val = adduint(NULL, db->curid);
  val = addstr(val, item->name);
  switch (item->type) {
	case item_string:
		val = addint(val, DBTYPE_STRING);
		val = addstr(val, item->val.s);
		val = addnull(val);
		val = addnull(val);
		break;
	case item_integer:
		val = addint(val, DBTYPE_INTEGER);
		val = addnull(val);
		val = addint(val, item->val.i);
		val = addnull(val);
		break;
	case item_boolean:
		val = addint(val, DBTYPE_BOOLEAN);
		val = addnull(val);
		val = addnull(val);
		val = addbool(val, item->val.b);
		break;
	case item_none:
	default:
		val = addint(val, DBTYPE_NONE);
		val = addnull(val);
		val = addnull(val);
		val = addnull(val);
		break;
  }

  /* Generate SQL command */
  str = "INSERT INTO %s (%s, %s, %s, %s, %s, %s) VALUES (%s)";
  ret = (char *)nbmalloc(strlen(str) + strlen(paramtable) +
					strlen(colnames[STR_SYSID]) +
					strlen(colnames[STR_PARAMNAME]) +
					strlen(colnames[STR_PARAMTYPE]) +
					strlen(colnames[STR_PARAMSTRVAL]) +
					strlen(colnames[STR_PARAMINTVAL]) +
					strlen(colnames[STR_PARAMBOOLVAL]) +
					strlen(val));
  sprintf(ret, str, paramtable,
			colnames[STR_SYSID],
			colnames[STR_PARAMNAME],
			colnames[STR_PARAMTYPE],
			colnames[STR_PARAMSTRVAL],
			colnames[STR_PARAMINTVAL],
			colnames[STR_PARAMBOOLVAL], val);
  free(val);
  return(ret);
}



/*
 * Write parameters for current record
 */
static void putparams __F((db, items), struct dbodbc *db AND struct dbitem *items)
{
  struct sqlexec *exec;
  struct dbitem *ip = items;
  char *sql;

  /* Check if we have a valid cursor position */
  if (!curid_valid(db->curid))
	return;

  /* Write each parameter into the database */
  while (ip != NULL) {
	sql = encode(db, ip);
	if ((exec = execute(db, sql)) == NULL) {
		free(sql);
		return;
	}
	endexec(exec);
	free(sql);
	ip = ip->next;
  }
}



/*
 * Add new system record
 */
static int addsysrec __F((db, newid, recname),
				struct dbodbc *db AND
				unsigned long newid AND
				const char *recname)
{
  struct sqlexec *exec;
  char *str, *sql, *val;
  char *systable = db->config->config.sql.systable;
  int ret = TRUE;

  /* Create SQL string for systems table */
  val = adduint(NULL, newid);
  val = addstr(val, recname);
  str = "INSERT INTO %s (%s, %s) VALUES (%s)";
  sql = (char *)nbmalloc(strlen(str) + strlen(systable) +
					strlen(colnames[STR_ID]) +
					strlen(colnames[STR_SYSNAME]) +
					strlen(val) + 1);
  sprintf(sql, str, systable, colnames[STR_ID], colnames[STR_SYSNAME], val);
  free(val);
  if ((exec = execute(db, sql)) == NULL)
	ret = FALSE;
  else
	endexec(exec);
  free(sql);
  return(ret);
}



/*
 * Check column names and types for a specific table
 */
static int checktable __F((db, table, layout),
					struct dbodbc *db AND
					char *table AND
					struct tablelayout *layout)
{
  SQLHSTMT stmt;
  struct sqlresult *result;
  int errnum, colnum, i, ret = FALSE;
  short columns[16], *s;
  char *cp, *name, *str;

  /* Determine number of columns and clear existance array */
  for (colnum = 0; layout[colnum].nameid != STR_INVALID; colnum++)
	columns[colnum] = FALSE;
  assert(colnum >= 0 && colnum <16);

  /* Create a new statement handle */
  errnum = SQLAllocHandle(SQL_HANDLE_STMT, db->conn, &stmt);
  if (!checkerr(errnum, "unable to create handle to get table columns"))
	return(FALSE);

  /* Get column info */
  errnum = SQLColumns(stmt, NULL, 0, NULL, 0,
					(SQLCHAR *)table, SQL_NTS, NULL, 0);
  if (!checkerr(errnum, "unable to get table column data"))
	goto tblerr1;

  /* Fetch all column info */
  result = allocresult(stmt);
  if (result == NULL)
	goto tblerr1;
  while ((errnum = SQLFetch(stmt)) != SQL_NO_DATA) {
	if (!checkerr(errnum, "unable to fetch table column data"))
		goto tblerr2;

	/* Check for column name */
	if ((name = getstrresult(result, STR_COLUMN_NAME)) == NULL)
		continue;
	for (i = 0; i < colnum; i++)
		if (!strcmp(colnames[layout[i].nameid], name))
			break;
	free(name);
	if (i == colnum)
		continue;

	/* Check for proper column data type */
	if ((s = getshortresult(result, STR_DATA_TYPE)) == NULL)
		continue;
	if (layout[i].sqltype != *s) {
		str = "invalid data type for column '%s' in table '%s'";
		goto tblerr3;
	}

	/* Mark column as found */
	columns[i] = TRUE;
  }

  /* Check that we found all columns */
  for (i = 0; i < colnum; i++)
	if (!columns[i]) {
		str = "missing column '%s' in table '%s'";
		name = colnames[layout[i].nameid];
		goto tblerr3;
	}
  ret = TRUE;
  goto tblerr2;

tblerr3:
  cp = (char *)nbmalloc(strlen(table) + strlen(name) + strlen(str));
  sprintf(cp, str, name, table);
  nblib_db_error(cp, DBERR_CLASS_ERROR);
  free(cp);

tblerr2:
  freeresult(result);

tblerr1:
  (void)SQLFreeHandle(SQL_HANDLE_STMT, stmt);
  return(ret);
}



/*
 * Function to be called at program termination
 */
static void closeenv __F_NOARGS
{
  if (sqlenv != SQL_NULL_HANDLE) {
	(void)SQLFreeHandle(SQL_HANDLE_ENV, sqlenv);
	sqlenv = SQL_NULL_HANDLE;
  }
}



/*
 * Initialize the database environment
 */
static int initmodule __F_NOARGS
{
  int errnum;

  /* Check if we are initialized already */
  if (sqlenv != SQL_NULL_HENV)
	return(TRUE);

  /* Create a new database environment */
  errnum = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &sqlenv);
  if (!checkerr(errnum, "unable to create database environment"))
	return(FALSE);

  /* Setup ODBC version */
  errnum = SQLSetEnvAttr(sqlenv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
  if (!checkerr(errnum, "unable to set ODBC version")) {
	closeenv();
	return(FALSE);
  }

  /* Return without error */
  return(TRUE);
}



/*
 * Open a ODBC database
 */
static DBID odbc_open __F((conf, flags), struct dbconfig *conf AND int flags)
{
  struct dbodbc *db = NULL;
  int errnum, class;

  /* Initialize the module */
  if (!initmodule())
	return(DBID_NULL);

  /* Check for flags */
  if (conf->mode == dbmode_ro) {
	/* This is just for safety - should have been done already */
	flags |= DBFLAGS_RO;
	flags &= ~DBFLAGS_ADD;
  }

  /* Set class for error messages */
  class = ((flags & DBFLAGS_OPT) != 0 ?  DBERR_CLASS_INFO : DBERR_CLASS_ERROR);

  /* Create a new database entity */
  assert(conf->type == dbtype_sql);
  db = (struct dbodbc *)nbmalloc(sizeof(struct dbodbc));
  db->flags = flags;
  db->config = conf;
  db->locked = FALSE;
  db->curid = CURID_BOF;
  db->mark = CURID_EOF;

  /* Create a new SQL connection handle */
  errnum = SQLAllocHandle(SQL_HANDLE_DBC, sqlenv, &(db->conn));
  if (!checkerr(errnum, "unable to create SQL connection"))
	goto openerr2;

  /*
   * Set access mode of SQL connection. If we just want to access the
   * database read-only, there is no need to open the connection with
   * read-write access.
   */
  if ((flags & DBFLAGS_RO) != 0) {
	errnum = SQLSetConnectAttr(db->conn, SQL_ATTR_ACCESS_MODE,
					(SQLPOINTER)SQL_MODE_READ_ONLY, 0);
	if (!checkerr(errnum, "unable to set SQL connection read-only"))
		goto openerr1;
  }

  /* Set login timeout */
  if (conf->config.sql.logintimeout >= 0) {
	errnum = SQLSetConnectAttr(db->conn, SQL_ATTR_LOGIN_TIMEOUT,
		(SQLPOINTER)((intptr_t)(conf->config.sql.logintimeout)), 0);
	if (!checkerr(errnum, "unable to set login timeout"))
		goto openerr1;
  }

  /* Set connection timeout */
  if (conf->config.sql.conntimeout >= 0) {
	errnum = SQLSetConnectAttr(db->conn, SQL_ATTR_CONNECTION_TIMEOUT,
		(SQLPOINTER)((intptr_t)(conf->config.sql.conntimeout)), 0);
	if (!checkerr(errnum, "unable to set connection timeout"))
		goto openerr1;
  }

  /*
   * Now actually connect to the database. If we are unable to connect to
   * the database, treat it not as an error if the database is marked
   * optional.
   */
  errnum = SQLConnect(db->conn,
			(SQLCHAR *)conf->config.sql.dsn, SQL_NTS,
			(SQLCHAR *)conf->config.sql.username, SQL_NTS,
			(SQLCHAR *)conf->config.sql.password, SQL_NTS);
  if (!checkerr(errnum, "unable to connect to SQL database"))
	goto openerr1;

  /* Check that the systems table has correct columns */
  if (!checktable(db, conf->config.sql.systable, syslayout))
	goto openerr3;

  /* Check that the parameter table has correct columns */
  if (!checktable(db, conf->config.sql.paramtable, paramlayout)) {
openerr3:
	(void)SQLDisconnect(db->conn);
openerr1:
	(void)SQLFreeHandle(SQL_HANDLE_DBC, db->conn);
openerr2:
	free(db);
	db = NULL;
  }

  /* Return database handle */
  if (db != NULL)
	opencount++;
  return(db == NULL ? DBID_NULL : (DBID)db);
}



/*
 * Close a ODBC database
 */
static void odbc_close __F((dbid), DBID dbid)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* Just for safety */
  assert(opencount > 0);

  /* Unlock the database */
  (void)lockdb(db, TRUE);

  /* Close the database */
  (void)SQLDisconnect(db->conn);
  (void)SQLFreeHandle(SQL_HANDLE_DBC, db->conn);

  /* Free the database handle */
  free(db);

  /* Check if we have to close the environment */
  if (--opencount == 0)
	closeenv();
}



/*
 * Release a ODBC cursor
 */
static void odbc_release __F((dbid), DBID dbid)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  (void)lockdb(db, TRUE);
  db->curid = CURID_BOF;
  db->mark = CURID_EOF;
}



/*
 * Find a record
 */
static int odbc_find __F((dbid, recname, first),
				DBID dbid AND
				const char *recname AND
				int first)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* Lock the database if necessary */
  if (!lockdb(db, FALSE))
	return(FALSE);

  /* Get next data set from database */
  return(getnext(db, recname, (first ? DIR_FIRST_REC : DIR_NEXT_REC)));
}



/*
 * Advance to next record
 */
static int odbc_next __F((dbid), DBID dbid)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* Lock the database if necessary */
  if (!lockdb(db, FALSE))
	return(FALSE);

  /* Get next data set from database */
  return(getnext(db, NULL, DIR_NEXT));
}



/*
 * Advance to previous record
 */
static int odbc_prev __F((dbid), DBID dbid)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* Lock the database if necessary */
  if (!lockdb(db, FALSE))
	return(FALSE);

  /* Get next data set from database */
  return(getnext(db, NULL, DIR_PREV));
}



/*
 * Advance to first record
 */
static int odbc_first __F((dbid), DBID dbid)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* Lock the database if necessary */
  if (!lockdb(db, FALSE))
	return(FALSE);

  /* Get next data set from database */
  return(getnext(db, NULL, DIR_FIRST));
}



/*
 * Advance to last record
 */
static int odbc_last __F((dbid), DBID dbid)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* Lock the database if necessary */
  if (!lockdb(db, FALSE))
	return(FALSE);

  /* Get next data set from database */
  return(getnext(db, NULL, DIR_LAST));
}



/*
 * Return name of current record
 */
static char *odbc_curpos __F((dbid), DBID dbid)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* Lock the database if necessary */
  if (!lockdb(db, FALSE))
	return(NULL);

  /* Read name of current record */
  return(getname(db));
}



/*
 * Mark the current record
 */
static int odbc_mark __F((dbid, action), DBID dbid AND int action)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* Lock the database if necessary */
  if (!lockdb(db, FALSE))
	return(FALSE);

  /* Execute required action */
  switch (action) {
	case BOOKMARK_SET:
		/* Check if the current cursor position is valid */
		if (!curid_valid(db->curid)) {
			nblib_db_error("invalid database position for bookmark",
							DBERR_CLASS_ERROR);
			break;
		}

		/* Set new bookmark */
		db->mark = db->curid;
		return(TRUE);

	case BOOKMARK_RELEASE:
		/* Make the old bookmark invalid */
		db->mark = CURID_EOF;
		return(TRUE);

	case BOOKMARK_JUMP:
		/* Check if we have anything to do */
		if (!curid_valid(db->mark))
			break;

		/* Set current cursor position with bookmark */
		db->curid = db->mark;
		db->mark = CURID_EOF;
		return(TRUE);

	default:
		nblib_db_error("invalid bookmark action code",
							DBERR_CLASS_ERROR);
		break;
  }
  return(FALSE);
}



/*
 * Delete current record
 */
static void odbc_delete __F((dbid), DBID dbid)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* This should have been checked in the upper levels already */
  assert((db->flags & DBFLAGS_RO) == 0);

  /* Unable to delete the beginning and end of file indications */
  if (!curid_valid(db->curid)) {
	nblib_db_error("invalid database delete position", DBERR_CLASS_ERROR);
	return;
  }

  /* If we are going to delete the marked record, invalidate the mark */
  if (db->curid == db->mark)
	db->mark = CURID_EOF;

  /* Delete the current record */
  if (!delsysrec(db, FALSE))
	return;

 /* Move cursor to next record */
  if (!getnext(db, NULL, DIR_NEXT))
	db->curid = CURID_EOF;
}



/*
 * Read a record from the current read position
 */
static struct dbitem *odbc_read __F((dbid, recname),
				DBID dbid AND
				char **recname)
{
  struct dbodbc *db = (struct dbodbc *)dbid;
  struct dbitem *ip;
  char *cp;

  /* Check for valid read position */
  if (!curid_valid(db->curid)) {
	nblib_db_error("invalid database read position", DBERR_CLASS_ERROR);
	return(NULL);
  }

  /* Determine current record name */
  if (recname == NULL || (cp = getname(db)) == NULL)
	return(NULL);

  /* Determine current parameter list */
  if ((ip = getparams(db)) == NULL) {
	free(cp);
	return(NULL);
  }

  /* Prepare return values */
  if (recname != NULL)
	*recname = cp;
  return(ip);
}



/*
 * Routine to determine the location of an error
 * This always returns NULL to use the default handling
 */
static char *odbc_errmsg __F((dbid, recname, item),
				DBID dbid AND
				const char *recname AND
				const struct dbitem *item)
{
  return(NULL);
}



/*
 * Write a record into the current position
 */
static void odbc_write __F((dbid, items),
				DBID dbid AND
				struct dbitem *items)
{
  struct dbodbc *db = (struct dbodbc *)dbid;

  /* This should have been checked in the upper levels already */
  assert((db->flags & DBFLAGS_RO) == 0);

  /* Empty records are not allowed */
  assert(items != NULL);

  /* Check if we have a current read position */
  if (!curid_valid(db->curid)) {
	nblib_db_error("error writing record", DBERR_CLASS_ERROR);
	return;
  }

  /* Delete old parameters */
  if (!delsysrec(db, TRUE))
	return;

  /* Add new record to database */
  putparams(db, items);
}



/*
 * Add a new record
 */
static void odbc_add __F((dbid, recname, items),
				DBID dbid AND
				const char *recname AND
				struct dbitem *items)
{
  struct dbodbc *db = (struct dbodbc *)dbid;
  unsigned int errnum;
  unsigned long newid;

  /* This should have been checked in the upper levels already */
  assert((db->flags & (DBFLAGS_ADD | DBFLAGS_RO)) == DBFLAGS_ADD);

  /* Empty records are not allowed */
  assert(items != NULL);

  /* Put the cursor to the last record */
  errnum = dberrors;
  if (!getnext(db, NULL, DIR_LAST)) {
	if (errnum < dberrors)
		return;
	assert(db->curid == CURID_EOF);
	newid = 1;
  } else
	newid = db->curid + 1;

  /* Add new system name to database */
  if (!addsysrec(db, newid, recname))
	return;
  db->curid = newid;

  /* Add new record to database */
  putparams(db, items);
}




/*
 * Structure to export all ODBC database functions
 */
struct dbprocs db_odbcprocs = {
	&odbc_open,
	&odbc_find,
	&odbc_next,
	&odbc_prev,
	&odbc_first,
	&odbc_last,
	&odbc_curpos,
	&odbc_mark,
	&odbc_delete,
	&odbc_read,
	&odbc_write,
	&odbc_add,
	&odbc_release,
	&odbc_close,
	&odbc_errmsg
};

#endif /* HAVE_ODBC */

