/* iodbcfuncs.c:  ODBC database compatibility layer. */

/* This file is part of <Meta-HTML>(tm), a system for the rapid
   deployment of Internet and Intranet applications via the use
   of the Meta-HTML language.

   Copyright (c) 1996, Universal Access Inc. (http://www.ua.com).
   Author: Henry Minsky (hqm@ua.com) Wed Oct  2 16:28:36 1996.

   Universal Access Inc.
   129 El Paseo Court
   Santa Barbara, CA
   93101
*/

/* ODBC compatibility layer */

/* ODBC includes from OpenLink Software drivers */

#if defined (OPENLINK_UDBC_CLIENT)
# include <libudbc.h>
/* The OpenLink SunOS UDBC libraries need this function for some reason. */
int __ansi_fflush (FILE *f) { return (fflush (f)); }
#else
# include <iodbc.h>
# include <isql.h>
# include <isqlext.h>
#endif /* OPENLINK_UDBC_CLIENT */

/* Generic ODBC declarations*/
#include <odbcdefs.h>

/****************************************************************/

#define DEFAULT_SQL_ESCAPE_CHARACTER '\''
#define DEFAULT_SQL_TRUNCATE_COLUMNS 1

PACKAGE_INITIALIZER (initialize_iodbc_functions)

PFunDesc iodbcfunc_table[] =
{
  { "ODBC::WITH-OPEN-DATABASE",		1, 0, pf_with_open_database },
  { "ODBC::DATABASE-EXEC-QUERY",	0, 0, pf_database_exec_query },
  { "ODBC::DATABASE-NEXT-RECORD",	0, 0, pf_database_next_record },
  { "ODBC::DATABASE-SAVE-RECORD",	0, 0, pf_database_save_record },
  { "ODBC::DATABASE-DELETE-RECORD",	0, 0, pf_database_delete_record },
  { "ODBC::DATABASE-LOAD-RECORD",	0, 0, pf_database_load_record },
  { "ODBC::DATABASE-SAVE-PACKAGE",	0, 0, pf_database_save_package },
  { "ODBC::NUMBER-OF-ROWS",   	        0, 0, pf_database_num_rows},
  { "ODBC::SET-ROW-POSITION",  	        0, 0, pf_database_set_pos},
  { "ODBC::DATABASE-QUERY",		0, 0, pf_database_query },
  { "ODBC::HOST-DATABASES",	        0, 0, pf_host_databases },
  { "ODBC::DATABASE-TABLES",     	0, 0, pf_database_tables },
  { "ODBC::DATABASE-TABLE-FIELDS",      0, 0, pf_database_table_fields },
  { "ODBC::DATABASE-FIELD-INFO",        0, 0, pf_database_field_info },
  { "ODBC::DATABASE-SET-OPTIONS",       0, 0, pf_database_set_options },
  { "ODBC::CURSOR-GET-COLUMN",          0, 0, pf_cursor_get_column },
  { "ODBC::QUERY-GET-COLUMN",           0, 0, pf_query_get_column },
  { "ODBC::SQL-TRANSACT",               0, 0, pf_sql_transact },
  { (char *)NULL,			0, 0, (PFunHandler *)NULL }
};

static void initialize_database_stack (void);

void
initialize_iodbc_functions (Package *package)
{
  register int i;
  Symbol *sym;

  for (i = 0; iodbcfunc_table[i].tag != (char *)NULL; i++)
    {
      sym = symbol_intern_in_package (package, iodbcfunc_table[i].tag);
      sym->type = symtype_FUNCTION;
      sym->values = (char **)(&iodbcfunc_table[i]);
    }

  initialize_database_stack ();
}

/****************************************************************
 * The Database object:
 *
 * Contains a stack of cursors, and information about the open
 * database connection.
 ****************************************************************/

typedef struct
{
  IStack *cursors;
  int connected;		/* 1 if db connection open, 0 otherwise */
  char sql_escape_char;
  int  sql_truncate_columns;

  /* ODBC-specific data */
  char   *dsn;			/* Data Source Name */
  HENV    henv;
  HDBC    hdbc;
  HSTMT   hstmt;

  /* Cache the saved information about fields of tables */
  struct gsql_result **table_cache_info;
  int table_cache_index;
  int table_cache_size;

} Database;

typedef struct
{
  char *name;
  char *table;
  int  type;
  int length;
  int nullable;
} gsql_field;

typedef struct
{
  HSTMT hstmt;
  int numfields;
  Database *db;
  gsql_field **fields;
  char *tablename;
} gsql_result;


/* For ODBC, a cursor points to a gsql_result object. */
typedef struct
{
  gsql_result *result;		/* The results of <database-exec-query..> */
  Database *db;			/* Associated database connection. */
} DBCursor;

static void free_database_cursors (Database *db);
static void gsql_free_result (gsql_result *result);

static void
free_cached_table_info (Database *db)
{
  int i;
  for (i = 0; i < db->table_cache_index; i++)
    gsql_free_result ((gsql_result *)db->table_cache_info[i]);
  xfree (db->table_cache_info);
}

static void
free_database_resources (Database *db)
{
  /* free cursors, namestrings, hdbc resources, etc */
  free_database_cursors (db);

  /* Free cached table information. */
  free_cached_table_info (db);
}

/* The index of where the next error message should be stored. */
static int odbc_error_index = 0;

static void
gsql_clear_error_message (void)
{
  odbc_error_index = 0;
  pagefunc_set_variable ("odbc::odbc-error-message[]", "");
}

/* Pass NULL to use system's error message. */
static void
gsql_save_error_message (Database *db, char *msg)
{
  unsigned char buf[256];
  unsigned char sqlstate[15];
  char odbc_error_variable[128];
  BPRINTF_BUFFER *err;

  /* Get statement errors */
  while (SQLError (db->henv, db->hdbc, db->hstmt, sqlstate, NULL,
      buf, sizeof(buf), NULL) == SQL_SUCCESS)
    {
      sprintf (odbc_error_variable, "odbc::odbc-error-message[%d]",
	       odbc_error_index);
      odbc_error_index++;

      err = bprintf_create_buffer ();
      bprintf (err, "HSTMT: SQLSTATE=%s %s\n", sqlstate, buf);
      pagefunc_set_variable (odbc_error_variable, err->buffer);
      bprintf_free_buffer (err);
    }

  /* Get connection errors */
  while (SQLError (db->henv, db->hdbc, SQL_NULL_HSTMT, sqlstate, NULL,
      buf, sizeof(buf), NULL) == SQL_SUCCESS)
    {
      sprintf (odbc_error_variable, "odbc::odbc-error-message[%d]",
	       odbc_error_index);
      odbc_error_index++;
      err = bprintf_create_buffer ();
      bprintf (err, "Connection: SQLSTATE=%s %s\n", sqlstate, buf);
      pagefunc_set_variable (odbc_error_variable,  err->buffer);
      bprintf_free_buffer (err);
    }

  /* Get environmental errors */
  while (SQLError (db->henv, SQL_NULL_HDBC, SQL_NULL_HSTMT, sqlstate, NULL,
      buf, sizeof(buf), NULL) == SQL_SUCCESS)
    {
      sprintf (odbc_error_variable, "odbc::odbc-error-message[%d]",
	       odbc_error_index);
      odbc_error_index++;
      err = bprintf_create_buffer ();
      bprintf (err, "Environment: SQLSTATE=%s %s\n", sqlstate, buf);
      pagefunc_set_variable (odbc_error_variable, err->buffer);
      bprintf_free_buffer (err);
    }

  if (msg != GSQL_DEFAULT_ERRMSG)
    {
      sprintf (odbc_error_variable, "odbc::odbc-error-message[%d]",
	       odbc_error_index);
      odbc_error_index++;
      pagefunc_set_variable (odbc_error_variable, msg);
    }
}

static int
gsql_number_of_rows (gsql_result *result)
{
  SDWORD nrows = 0;

  gsql_clear_error_message ();

  if (result->hstmt != (HSTMT) NULL)
    {
      if (SQLRowCount (result->hstmt, &nrows) != SQL_SUCCESS)
	gsql_save_error_message (result->db, "SQLRowCount");
    }

  return ((int) nrows);
}

static void
gsql_data_seek (gsql_result *result, int position)
{
  /* Use SQLSetPos, if you can figure out how. */
}

static int
odbc_to_gsql_status (int status)
{
  switch (status)
    {
    case SQL_INVALID_HANDLE:	return (GSQL_INVALID_HANDLE);
    case SQL_ERROR:		return (GSQL_ERROR);
    case SQL_SUCCESS:		return (GSQL_SUCCESS);
    case SQL_SUCCESS_WITH_INFO:	return (GSQL_SUCCESS_WITH_INFO);
    case SQL_NO_DATA_FOUND:	return (GSQL_NO_DATA_FOUND);
    default:			return (GSQL_ERROR);
    }
}

static int
gsql_fetch_row (gsql_result *result)
{
  int status = SQLFetch (result->hstmt);
  return (odbc_to_gsql_status (status));
}

static gsql_field *
gsql_fetch_field (gsql_result *result, int i)
{
  if (result->fields != (gsql_field **) NULL)
    return (result->fields[i]);
  else
    return ((gsql_field *)NULL);
}

/* We allocate a new hstmt for this query, and leave it sitting in the
   db->hstmt slot.

   We need to free these things eventually, but that is the job of
   gsql_store_result() and gsql_free_result().  gsql_store_result () copies
   the hstmt pointer to a gsql_result structure, and sets the db->hstmt
   slot to NULL.  gsql_free_result is responsible for calling SQLFreeStmt ().

   Thus, a call to gsql_query () should always be followed by a call to
   gsql_store_result (), and the result must eventually be freed using
   gsql_free_result (). */
static int
gsql_query (Database *db, char *query)
{
  if (db->connected)
    {
      /* If there is an old hstmt sitting here, free it. */
      if (db->hstmt)
	SQLFreeStmt (db->hstmt, SQL_DROP);

      if (SQLAllocStmt (db->hdbc, &(db->hstmt)) != SQL_SUCCESS)
	return (GSQL_ERROR);

      if (SQLExecDirect (db->hstmt, (UCHAR *) query, SQL_NTS) != SQL_SUCCESS)
	{
	  gsql_save_error_message (db, "SQLExecDirect");
	  return (GSQL_ERROR);
	}
      else
	return (GSQL_SUCCESS);
    }
  else
    return (GSQL_ERROR);
}

static void
free_gsql_field (gsql_field *field)
{
  if (field != (gsql_field *)NULL)
    {
      xfree (field->name);
      xfree (field->table);
      free (field);
    }
}

static void
gsql_free_result (gsql_result *result)
{
  int i;
  HSTMT hstmt = result->hstmt;

  if (result->fields != (gsql_field **) NULL)
    {
      i = 0;
      while (result->fields[i] != (gsql_field *) NULL)
	{
	  /* Free the gsql_field structs.  */
	  free_gsql_field (result->fields[i]);
	  i++;
	}
      free (result->fields);
    }

  xfree (result->tablename);

  if (hstmt)
    SQLFreeStmt (hstmt, SQL_DROP);

  free (result);
}

/* Initialize the ODBC-specific portions of the database structure. */
static void
initialize_database (Database *db)
{
  db->henv = NULL;
  db->hdbc = NULL;
  db->hstmt = NULL;
  db->connected = 0;
  db->table_cache_index = 0;
  db->table_cache_size = 10;
  db->table_cache_info =
    (struct gsql_result **) xmalloc (10 * sizeof (gsql_result *));

  db->sql_escape_char =  DEFAULT_SQL_ESCAPE_CHARACTER;
  db->sql_truncate_columns = DEFAULT_SQL_TRUNCATE_COLUMNS;
}

static void
database_add_cached_table_info (Database *db, gsql_result *gr)
{
  if (db->table_cache_index + 2 > db->table_cache_size)
    db->table_cache_info = (struct gsql_result  **)xrealloc
      (db->table_cache_info, ((db->table_cache_size += 5) * sizeof (gsql_result *)));

  db->table_cache_info[db->table_cache_index++] = (struct gsql_result *) gr;

}

/* Search for the first result with a matching tablename.  */
static gsql_result *
database_lookup_cached_table_info (Database *db, char *tablename)
{
  register int i;

  for (i = 0; i < db->table_cache_index; i++)
    {
      gsql_result *gr;
      gr = (gsql_result *)db->table_cache_info[i];
      if ((gr->tablename != (char *) NULL) &&
	  (strcasecmp (tablename, gr->tablename) == 0))
	return (gr);
    }

  return ((gsql_result *) NULL);
}


static gsql_result *
make_gsql_result (void)
{
  gsql_result *g = (gsql_result *)xmalloc (sizeof (gsql_result));

  g->hstmt = (HSTMT) NULL;
  g->fields = (gsql_field **) NULL;
  g->numfields = -1;
  g->tablename = (char *) NULL;

  return (g);
}

/* Reads the field info using SQLDescribeCol and creates
   a gsql_field with the descriptive information filled in. */
static gsql_field *
get_gsql_field_from_result (gsql_result *result, int colnum)
{
  gsql_field *gfield = (gsql_field *)xmalloc (sizeof (gsql_field));
  short colType;
  UDWORD colPrecision;
  short colScale;
  short colNullable;
  char colName[512];
  RETCODE status;

  colName[0] = '\000';

  gfield->name = (char *) NULL;
  gfield->type = 0;
  gfield->length = 0;
  gfield->nullable = 0;

  /* Get column name */
  status = SQLDescribeCol
    (result->hstmt, colnum, (UCHAR *) colName,
     sizeof (colName), NULL, &colType, &colPrecision,
     &colScale, &colNullable);
  if (status != SQL_SUCCESS)
    {
      gsql_save_error_message (result->db, "SQLDescribeCol");
    }
  else
    {
      gfield->table = (char *)NULL;
      gfield->name = strdup (colName);
      gfield->type = colType;
      gfield->length = colPrecision;
      gfield->nullable = colNullable;
    }

  return (gfield);
}

/* Loop over the columns of this set, and create a field structure for
   each column. This is done by using the SQLDescribeCol() function,
   to probe each column in sequence.*/
static gsql_result *
gsql_make_field_array (gsql_result *gr)
{
  gsql_field *gfield;
  SWORD numfields;
  int i;

  if (SQLNumResultCols (gr->hstmt, &numfields) != SQL_SUCCESS)
    return (NULL);

  gr->numfields = numfields;

  if (numfields > 0)
    {
      gr->fields = xmalloc ((numfields + 1) * sizeof (gsql_field *));

      for (i = 0; i < numfields; i++)
	{
	  /* ODBC column numbers start at 1, not 0 */
	  gfield = get_gsql_field_from_result (gr, i+1);
	  gr->fields[i] = gfield;
	}

      gr->fields[i] = (gsql_field *) NULL;
    }

  return (gr);
}



/* We need to create an array of gsql_field structs which have the
   column info. We iterate over the columns, and create a gsql_field
   object containing the info about that field. */
static gsql_result *
gsql_store_result (Database *db)
{
  gsql_result *gr = make_gsql_result ();

  gr->db = db;
  if (db->hstmt != NULL)
    {
      gr->hstmt = db->hstmt;	/* Copy the hstmt to the result */
      db->hstmt = NULL;
      gsql_make_field_array (gr);
      return (gr);
    }
  else
    {
      gsql_free_result (gr);
      return ((gsql_result *)NULL);
    }
}

/* We need also a separate database-table-info command, like the
 database-field-info command, which you can use to get more info
 about a specific table. */
static gsql_result *
gsql_db_list_tables (Database *db)
{
  gsql_result *gr = (gsql_result *) NULL;
  HSTMT hstmt;
  RETCODE status;
  /* Issue an SQLTables request, and then create a result set. */
  gsql_clear_error_message ();

  if (SQLAllocStmt (db->hdbc, &hstmt) != SQL_SUCCESS)
    {
      gsql_save_error_message (db, "SQLAllocStmt");
      return ((gsql_result *)NULL);
    }

  status = SQLTables (hstmt,
		      "", SQL_NTS,
		      "", SQL_NTS,
		      "", SQL_NTS,
		      "", SQL_NTS);

  if (status != SQL_SUCCESS)
    {
      gsql_save_error_message (db, "SQLTables");
      return ((gsql_result *)NULL);
    }

  /* gsql_store_result() will copy the HSTMT to a result struct,
     and it will eventually be freed when someone calls gsql_free_result. */
  db->hstmt = hstmt;
  gr = gsql_store_result (db);

  return (gr);
}

/****************************************************************
 * Field properties
 *
 * name, length, datatype, is_primary_key, not_null
 *
 ****************************************************************/

#if defined (MACRO_REFERENCE_DISALLOWED)
static char *
gsql_field_name (gsql_field *field) { return (field->name); }

static char *
gsql_field_table (gsql_field *field) { return (field->table); }

/* We happen to be using the identical ANSI ODBC field types in
   gensqlfuncs.c, so we can just pass them through. */
static int gsql_field_type (gsql_field *field) { return ((field->type)); }
static int gsql_field_length (gsql_field *field) { return (field->length); }
static int gsql_field_is_primary_key (gsql_field *field) { return (0); }
static int
gsql_field_is_not_null (gsql_field *field)
{
  return (field->nullable == SQL_NO_NULLS);
}
#else
#  define gsql_field_name(field) (field->name)
#  define gsql_field_table(field) (field->table)
#  define gsql_field_type(field) (field->type)
#  define gsql_field_length(field) (field->length)
#  define gsql_field_is_primary_key(field) 0
#  define gsql_field_is_not_null(field) (field->nullable == SQL_NO_NULLS)
#endif /* !MACRO_REFERENCE_DISALLOWED */

/* This needs to build the array of gsql_fields.

   The resulting array of fields is cached in the database struct.

   Thus, the caller should *not* attempt to free the gsql_result object
   which is returned from this function, since it needs to persist in the
   database table info cache until the database is closed.
*/
static gsql_result *
gsql_list_fields (Database *db, char *tablename)
{
  gsql_result *gr;

#define STR_LEN 128+1
#define REM_LEN 254+1

  HSTMT hstmt;
  UCHAR szQualifier[STR_LEN], szOwner[STR_LEN];
  UCHAR szTableName[STR_LEN], szColName[STR_LEN];
  UCHAR szTypeName[STR_LEN], szRemarks[REM_LEN];
  SDWORD Precision, Length;
  SWORD DataType, Scale, Radix, Nullable;

  SDWORD cbQualifier, cbOwner, cbTableName, cbColName;
  SDWORD cbTypeName, cbRemarks, cbDataType, cbPrecision;
  SDWORD cbLength, cbScale, cbRadix, cbNullable;

  RETCODE retcode;
  int i;

  gr = database_lookup_cached_table_info (db, tablename);

  if (gr != (gsql_result *) NULL)
    return gr;

  gr = make_gsql_result ();

  if (SQLAllocStmt (db->hdbc, &hstmt) != SQL_SUCCESS)
    return ((gsql_result *)NULL);

  retcode = SQLColumns (hstmt,
			NULL, 0, /* All qualifiers */
			NULL, 0, /* All owners */
			tablename, SQL_NTS, /* Table name */
			NULL, 0); /* All columns */

  if (retcode == SQL_SUCCESS)
    {
      int arraysize = 10;
      /* We get back one descriptive row for each column in the table. */
      gr->fields = (gsql_field **) xmalloc
	((arraysize + 1) * sizeof (gsql_field *));

      SQLBindCol (hstmt,  1, SQL_C_CHAR,   szQualifier, STR_LEN, &cbQualifier);
      SQLBindCol (hstmt,  2, SQL_C_CHAR,   szOwner, STR_LEN, &cbOwner);
      SQLBindCol (hstmt,  3, SQL_C_CHAR,   szTableName, STR_LEN, &cbTableName);
      SQLBindCol (hstmt,  4, SQL_C_CHAR,   szColName, STR_LEN, &cbColName);
      SQLBindCol (hstmt,  5, SQL_C_SHORT, &DataType, 0, &cbDataType);
      SQLBindCol (hstmt,  6, SQL_C_CHAR,   szTypeName, STR_LEN, &cbTypeName);
      SQLBindCol (hstmt,  7, SQL_C_LONG,  &Precision, 0, &cbPrecision);
      SQLBindCol (hstmt,  8, SQL_C_LONG,  &Length, 0, &cbLength);
      SQLBindCol (hstmt,  9, SQL_C_SHORT, &Scale, 0, &cbScale);
      SQLBindCol (hstmt, 10, SQL_C_SHORT, &Radix, 0, &cbRadix);
      SQLBindCol (hstmt, 11, SQL_C_SHORT, &Nullable, 0, &cbNullable);
      SQLBindCol (hstmt, 12, SQL_C_CHAR,   szRemarks, REM_LEN, &cbRemarks);

      for (i = 0; ; i++)
	{
	  retcode = SQLFetch (hstmt);
	  if (retcode == SQL_SUCCESS)
	    {
	      /* create fields... */

	      gsql_field *gfield = (gsql_field *)xmalloc (sizeof (gsql_field));

	      gfield->name = strdup (szColName);
	      gfield->type = DataType;
	      gfield->length = Length;
	      gfield->nullable = Nullable;

	      if (i + 2 > arraysize)
		    gr->fields = (gsql_field  **)xrealloc
		      (gr->fields, ((arraysize += 5) * sizeof (gsql_field *)));

	      gr->fields[i] = gfield;
	    }
	  else
	    break;
	}

      gr->fields[i] = (gsql_field *) NULL;
      gr->numfields = i;
      gr->tablename = strdup (tablename);
    }

  if (hstmt)
    SQLFreeStmt (hstmt, SQL_DROP);

  database_add_cached_table_info (db, gr);

  return (gr);
}

#if defined (MACRO_REFERENCE_DISALLOWED)
/* The number of fields in a result row. */
static int gsql_num_fields (gsql_result *r) { return (r->numfields); }
#else
#  define gsql_num_fields(r) (r->numfields)
#endif

/* Fetch data from RESULT at column COL.  The result string is limited
   to 64k.  The return code SQL_SUCCESS_WITH_INFO is supposed be used
   if needed to get sequential chunks of data which are longer than
   the fetchBuffer can hold. A realloc loop should be implemented as a
   future improvement to get arbitrary length data, but be sure to
   establish that the ODBC drivers you are using actually support for
   this functionality.

   Always conses a new string or NULL. */
static char *
gsql_get_column (gsql_result *result, int col)
{
  SDWORD colIndicator;
  char fetchBuffer[65536];
  RETCODE retcode;

  /*  Fetch this column as string data. */
  retcode = SQLGetData (result->hstmt, col+1, SQL_CHAR, fetchBuffer,
			sizeof (fetchBuffer), &colIndicator);

  if ((retcode == SQL_SUCCESS) || (retcode == SQL_SUCCESS_WITH_INFO))
    return (strdup (fetchBuffer));
  else
    return ((char *)NULL);
}

/* From the result set of a msqlListTables command,
   return the column which has the table name.  */
static char *
gsql_get_column_table_name (gsql_result *gr)
{
  /* The columns in a result set returned by SQLTables() are
       TABLE_QUALIFER
       TABLE_OWNER
       TABLE_NAME
       TABLE_TYPE

   So, we want to return column 2 because we number columns starting
   at 0, not 1, and gsql_get_column () takes care of fixing the index. */
  return (gsql_get_column (gr, 2));
}

static void
gsql_connect (char *dsn, Database *db)
{
  RETCODE status;

  db->connected = 0;

  if (dsn != (char *) NULL)
    {
      char buf[257];
      short buflen;

      db->dsn = strdup (dsn);

      if (SQLAllocEnv (&(db->henv)) == SQL_SUCCESS)
	if (SQLAllocConnect (db->henv, &(db->hdbc)) == SQL_SUCCESS)
	  {

	    status =
	      SQLDriverConnect (db->hdbc, 0,
				(UCHAR *) dsn, SQL_NTS,
				(UCHAR *) buf,
				sizeof (buf), &buflen, SQL_DRIVER_COMPLETE);

	    if (status != SQL_SUCCESS && status != SQL_SUCCESS_WITH_INFO)
	      return;

	    db->connected = 1;
	  }
    }
}

static void
gsql_close (Database *db)
{
  if (db->connected == 1)
    {
      if (db->hdbc)
	SQLDisconnect (db->hdbc);

      if (db->hdbc)
	SQLFreeConnect (db->hdbc);

      if (db->henv)
	SQLFreeEnv (db->henv);

      xfree (db->dsn);
    }
}


/* <odbc::host-databases [hostname] [result=varname]>
   Returns an array of the sources available from the SQL driver.

   The HOSTNAME is for compatibility with msql functions and has no
   effect. All information is retrieved from the ODBC client driver
   on the local system.

   If VARNAME is supplied, the array is placed into that variable instead. */
static void
pf_host_databases (PFunArgs)
{
#if defined (USE_OPENLINK_SQLDATASOURCES_BROKEN_DRIVERS)
  char *resultvar = mhtml_evaluate_string (get_value (vars, "result"));
  RETCODE status;
  HENV    henv;
  UCHAR source_name[1024];
  UCHAR source_desc[1024];
  SWORD size1, size2;

  /* No errors yet! */
  gsql_clear_error_message ();

  /* SQLDataSources doesn't seem to work in OpenLink's drivers.  -- hqm */

  if (SQLAllocEnv (&henv) == SQL_SUCCESS)
    {

      status = SQLDataSources (henv, SQL_FETCH_FIRST,
			       source_name, sizeof (source_name), &size1,
			       source_desc, sizeof (source_desc), &size2);

      if (status == SQL_SUCCESS)
	{
	  int count = 0;
	  int namesize = 10;
	  char result[1024];
	  char **names = (char **) xmalloc ((namesize + 1) * sizeof (char *));

	  while (1)
	    {
	      if (count + 2 > namesize)
		    names = (char  **)xrealloc
		      (names, ((namesize += 5) * sizeof (char *)));

	      result[0] = 0;
	      strcat (result, source_name);
	      strcat (result, ":");
	      strcat (result, source_desc);

	      names[count] = strdup (result);
	      count++;

	      status =
		SQLDataSources (henv, SQL_FETCH_NEXT,
				source_name, sizeof (source_name), &size1,
				source_desc, sizeof (source_desc), &size2);

	      if (status != SQL_SUCCESS)
		break;
	    }

	  if (!empty_string_p (resultvar))
	    {
	      symbol_store_array (resultvar, names);
	    }
	  else
	    {
	      register int i;

	      for (i = 0; i < count; i++)
		{
		  bprintf_insert (page, start, "%s\n", names[i]);
		  start += 1 + strlen (names[i]);
		  free (names[i]);
		}
	      free (names);
	      *newstart = start;
	    }
	}

	SQLFreeEnv (henv);
    }

  xfree (resultvar);
#endif /* USE_OPENLINK_SQLDATASOURCES_BROKEN_DRIVERS */
}

static int
gsql_transact_internal (Database *db, char *action_arg)
{
  int action;
  RETCODE status;

  if (!empty_string_p (action_arg) && (!strcasecmp (action_arg, "ROLLBACK")))
    action = SQL_ROLLBACK;
  else
    action = SQL_COMMIT;

  status = SQLTransact (db->henv, db->hdbc, action);
  return (odbc_to_gsql_status (status));
}
