/* msqldb.c:  MSQL 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.

   Meta-HTML is free software; you can redistribute it and/or modify
   it under the terms of the UAI Free Software License as published
   by Universal Access Inc.; either version 1, or (at your option) 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
   UAI Free Software License for more details.

   You should have received a copy of the UAI Free Software License
   along with this program; if you have not, you may obtain one by
   writing to:

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

#include <msql.h>

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

#define DEFAULT_SQL_ESCAPE_CHARACTER  '\\'
#define DEFAULT_SQL_TRUNCATE_COLUMNS  1
#define DEFAULT_SQL_PREFIX_TABLENAMES 0

PACKAGE_INITIALIZER (initialize_msql_functions)

PFunDesc msqlfunc_table[] =
{
  { "MSQL::WITH-OPEN-DATABASE",		1, 0, pf_with_open_database },
  { "MSQL::DATABASE-EXEC-QUERY",	0, 0, pf_database_exec_query },
  { "MSQL::DATABASE-EXEC-SQL",   	0, 0, pf_database_exec_sql },
  { "MSQL::DATABASE-NEXT-RECORD",	0, 0, pf_database_next_record },
  { "MSQL::DATABASE-SAVE-RECORD",	0, 0, pf_database_save_record },
  { "MSQL::DATABASE-DELETE-RECORD",	0, 0, pf_database_delete_record },
  { "MSQL::DATABASE-LOAD-RECORD",	0, 0, pf_database_load_record },
  { "MSQL::DATABASE-SAVE-PACKAGE",	0, 0, pf_database_save_package },
  { "MSQL::NUMBER-OF-ROWS",   	        0, 0, pf_database_num_rows},
  { "MSQL::SET-ROW-POSITION",  	        0, 0, pf_database_set_pos},
  { "MSQL::DATABASE-QUERY",		0, 0, pf_database_query },
  { "MSQL::HOST-DATABASES",	        0, 0, pf_host_databases },
  { "MSQL::DATABASE-TABLES",     	0, 0, pf_database_tables },
  { "MSQL::DATABASE-TABLES-INFO",     	0, 0, pf_database_tables_info },
  { "MSQL::DATABASE-COLUMNS",           0, 0, pf_database_columns },
  { "MSQL::DATABASE-COLUMN-INFO",       0, 0, pf_database_column_info },
  { "MSQL::DATABASE-COLUMNS-INFO",      0, 0, pf_database_columns_info },
  { "MSQL::DATABASE-QUERY-INFO",        0, 0, pf_database_query_info },
  { "MSQL::DATABASE-SET-OPTIONS",       0, 0, pf_database_set_options },
  { "MSQL::CURSOR-GET-COLUMN",          0, 0, pf_cursor_get_column },
  { "MSQL::QUERY-GET-COLUMN",           0, 0, pf_query_get_column },
  { "MSQL::SQL-TRANSACT",               0, 0, pf_sql_transact },
  { (char *)NULL,			0, 0, (PFunHandler *)NULL }
};

static void initialize_database_stack (void);

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

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

  initialize_database_stack ();
}

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

/* Look up a key-value pair in a ODBC style DSN string.

   Example:
       HOST=star;SVT=Informix 5;UID=demo;PWD=demo;DATABASE=stores5 */
static char *
dsn_lookup (char *key, char *str)
{
  char *dsn = strdup (str);
  char *kp = dsn;
  char *vp = (char *)NULL;
  char *val = (char *)NULL;
  int found = 0;

  while ((vp = strchr (kp, '=')) != (char *) NULL)
    {
      char *semi;

      *vp = 0;
      vp++;

      /* Trim off semicolon separator, if there is one */
      semi = strchr (vp, ';');
      if (semi != (char *)NULL)
	*semi = 0;

      if (strcasecmp (kp, key) == 0)
	{
	  found = 1;
	  break;
	}

      if (semi == (char *)NULL)
	break;
      else
	kp = ++semi;
    }

  if (found == 1)
    val = strdup (vp);
  else
    val = (char *)NULL;

  free (dsn);
  return (val);
}

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

  /* MSQL-specific data */
  int sock;
  char *dbname;
  char *hostname;

} Database;

typedef struct
{
  m_field *msql_field;
  /* These are not used by anyone yet, but are for 
     future compatibility with ODBC. */
  char *qualifier;
  char *owner;
  char *typename;
  int precision;
  int scale;
  int radix;

} gsql_field;

typedef struct
{
  m_result *msql_result;
  m_row msql_row;
  gsql_field **fields;
} gsql_result;


/* For MSQL, a cursor points to a m_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
free_database_resources (Database *db)
{
  /* free cursors, namestrings, hdbc resources, etc */
  free_database_cursors (db);
}

/* Index where the next error message should go. */
static int msql_error_index = 0;

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

/* Pass NULL to use system's error message. */
static void
gsql_save_error_message (Database *db, char *msg)
{
  char error_variable[128];

  sprintf (error_variable, "msql::msql-error-message[%d]", msql_error_index);
  msql_error_index++;

  if (msg == GSQL_DEFAULT_ERRMSG)
    {
      pagefunc_set_variable (error_variable, msqlErrMsg);
      page_debug ("MSQL: %s", msqlErrMsg);
    }
  else
    {
      pagefunc_set_variable (error_variable, msg);
      page_debug ("MSQL: %s", msg);
    }
}

static int
gsql_number_of_rows (gsql_result *result)
{
  return (msqlNumRows (result->msql_result));
}

void
gsql_data_seek (gsql_result *result, int position)
{
  if (result->msql_result != (m_result *)NULL)
    msqlDataSeek (result->msql_result, position);
}

static int
gsql_fetch_row (gsql_result *result)
{
  result->msql_row =
    msqlFetchRow (result->msql_result);

  if (result->msql_row != (m_row)NULL)
    return (GSQL_SUCCESS);
  else
    return (GSQL_NO_DATA_FOUND);
}

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);
}

static int
gsql_query (Database *db, char *query)
{
  int status =  msqlQuery(db->sock, query);
  if (status == -1)
    {
      gsql_save_error_message (db, "msqlQuery");
      return (GSQL_ERROR);
    }
  else
    return (GSQL_SUCCESS);
}

static void
gsql_free_result (gsql_result *result)
{
  int i;
  m_result *mr = result->msql_result;

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

  if (mr != (m_result *)NULL) msqlFreeResult (mr);

  free (result);
}

static void
initialize_database (Database *db)
{
  db->dbname = (char *) NULL;
  db->hostname = (char *) NULL;
  db->sock= -1;

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

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

  g->msql_result = (m_result *) NULL;
  g->msql_row = (m_row) NULL;
  g->fields = (gsql_field **) NULL;

  return (g);
}

static void
initialize_gsql_field (gsql_field *gfield)
{
  gfield->qualifier = (char *) NULL;
  gfield->owner     = (char *) NULL;
  gfield->typename  = (char *) NULL;
  gfield->precision = 0;
  gfield->scale     = 0;
  gfield->radix     = 0;
}


static gsql_result *
gsql_make_field_array (m_result *result)
{
  gsql_result * gr = make_gsql_result ();
  gsql_field *gfield;
  m_field *mfield;
  int numfields, i;

  gr->msql_result = result;

  msqlFieldSeek (result, 0);
  numfields = msqlNumFields (result);

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

      for (i = 0; i < numfields; i++)
	{
	  gfield = (gsql_field *)xmalloc (sizeof (gsql_field));
	  initialize_gsql_field (gfield);
	  mfield = msqlFetchField (gr->msql_result);
	  gfield->msql_field = mfield;
	  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 traverse the MSQL field list, and
   wrap each field into a gsql_field object. */
static gsql_result *
gsql_store_result (Database *db)
{
  m_result *mr = msqlStoreResult ();
  gsql_result *gr = (gsql_result *) NULL;

  if (mr != (m_result *) NULL)
    {
      gr = gsql_make_field_array (mr);
    }
  return (gr);
}

/* The options  qualifier,owner,name,type are for ODBC
   compatibility. */
static gsql_result *
gsql_db_list_tables (Database *db,
		     char *table_qualifier, 
		     char *table_owner,
		     char *table_name,
		     char *table_type)
{
  gsql_result *gr = (gsql_result *) NULL;
  m_result *mr = msqlListTables (db->sock);

  if (mr != (m_result *) NULL)
    {
      gr = gsql_make_field_array (mr);
    }
  return (gr);
}

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

/* MSQL 2.0 uses UNIQUE fields instead of a primary key field. */
#ifndef IS_UNIQUE
#  define IS_UNIQUE IS_PRI_KEY
#endif

#define gsql_field_name(f) (f->msql_field->name)
#define gsql_field_table(f) (f->msql_field->table)
#define gsql_field_length(f) (f->msql_field->length)
#define gsql_field_is_primary_key(f) (IS_PRI_KEY (f->msql_field->flags))
#define gsql_field_is_unique(f) (IS_UNIQUE (f->msql_field->flags))
#define gsql_field_is_not_null(f) (IS_NOT_NULL (f->msql_field->flags))

/* Future ODBC compatibility */
#define gsql_field_qualifier(f) (f->qualifier)
#define gsql_field_owner(f) (f->owner)
#define gsql_field_typename(f) (f->typename)
#define gsql_field_precision(f) (f->precision)
#define gsql_field_scale(f) (f->scale)
#define gsql_field_radix(f) (f->radix)

static int
gsql_field_type (gsql_field *field)
{
  switch (field->msql_field->type)
    {
#if defined (CHAR_TYPE)
    case CHAR_TYPE:	return (GSQL_CHAR);
#endif
#if defined (INT_TYPE)
    case INT_TYPE:	return (GSQL_INTEGER);
#endif
#if defined (REAL_TYPE)
    case REAL_TYPE:	return (GSQL_REAL);
#endif
#if defined (IDX_TYPE)
    case IDX_TYPE:      return (GSQL_IDX);
#endif
#if defined (TEXT_TYPE)
    case TEXT_TYPE:     return (GSQL_VARCHAR);
#endif
#if defined (NULL_TYPE)
    case NULL_TYPE:     return (GSQL_NULL);
#endif
#if defined (IDENT_TYPE)
    case IDENT_TYPE:    return (GSQL_IDENT);
#endif
#if defined (UINT_TYPE)
    case UINT_TYPE:     return (GSQL_UINT);
#endif
#if defined (ANY_TYPE)
    case ANY_TYPE:      return (GSQL_ANY);
#endif
#if defined (SYSVAR_TYPE)
    case SYSVAR_TYPE:   return (GSQL_SYSVAR);
#endif
    default:		return (GSQL_ANY);
    }
}

/* This needs to build the array of gsql_fields. */
static gsql_result *
gsql_list_fields (Database *db, char *tablename)
{
  m_result *mr = msqlListFields (db->sock, tablename);
  gsql_result *gr = (gsql_result *) NULL;

  if (mr != (m_result *) NULL)
    {
      gr = gsql_make_field_array (mr);
    }
  return (gr);
}

static int
gsql_num_fields (gsql_result *result)
{
  if (result->msql_result != (m_result *) NULL)
    return (msqlNumFields (result->msql_result));
  else
    return (0);
}

/* Fetch data from RESULT at column COL.
   A new string is consed.*/
static char *
gsql_get_column (gsql_result *result, int col)
{
  if (result->msql_row[col])
    return (strdup (result->msql_row[col]));
  else
    return ((char *)NULL);
}

/* From the result set of a msqlListTables command,
   return the column which has the table name.  This
   is column 0. */
static char *
gsql_get_column_table_name (gsql_result *gr)
{
  return (gsql_get_column (gr, 0));
}


static void
gsql_connect (char *dsn, Database *db)
{
  int sock;
  char *dbhost = dsn_lookup ("host", dsn);
  char *dbname = dsn_lookup ("database", dsn);

  db->connected = 0;

  db->dbname = dbname;
  db->hostname = dbhost;

  if (dbhost != (char *) NULL)
    {
      sock = msqlConnect (dbhost);

      db->sock = sock;
      if (sock != -1)
	{
	  if (msqlSelectDB (sock, dbname) == -1)
	    gsql_save_error_message (db, GSQL_DEFAULT_ERRMSG);
	  else
	    db->connected = 1;
	}
    }
}

static void
gsql_close (Database *db)
{
  if (db->connected == 1)
    {
      msqlClose (db->sock);
    }
}

/* <msql::host-databases [hostname] [result=varname]>
   Returns an array of the databases available on HOST.
   If VARNAME is supplied, the array is placed into that variable instead. */
static void
pf_host_databases (PFunArgs)
{
  char *host = mhtml_evaluate_string (get_positional_arg (vars, 0));
  char *resultvar = mhtml_evaluate_string (get_value (vars, "result"));
  int sock;

  /* No errors yet! */
  pagefunc_set_variable ("msql::msql-error-message[]", "");

  if (empty_string_p (host))
    {
      xfree (host);
      host = strdup ("localhost");
    }

  if ((sock = msqlConnect (host)) > -1)
    {
      m_result *result = msqlListDBs (sock);
      int nrows = (result ? msqlNumRows (result) : 0);

      if (nrows != 0)
	{
	  int count = 0;
	  char **dbnames = (char **) xmalloc ((nrows + 1) * sizeof (char *));
	  m_row msql_row;

	  /* Loop over rows returned; the db name will be passed in the first
	     field of each 'row'.  Add names to the result array.  */
	  while ((msql_row = msqlFetchRow (result)) != (m_row)NULL)
	    dbnames[count++] = strdup (msql_row[0]);

	  dbnames[count] = (char *) NULL;

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

	      for (i = 0; dbnames[i] != (char *)NULL; i++)
		{
		  bprintf_insert (page, start, "%s\n", dbnames[i]);
		  start += 1 + strlen (dbnames[i]);
		  free (dbnames[i]);
		}
	      free (dbnames);
	      *newstart = start;
	    }
	}

      if (result != (m_result *)NULL) msqlFreeResult (result);
      msqlClose (sock);
    }
  else
    {
      pagefunc_set_variable ("msql::msql-error-message[]", msqlErrMsg);
    }

  xfree (host);
  xfree (resultvar);
}

/* Transactions are not yet implemented for MSQL */
static int
gsql_transact_internal (Database *db, char *action_arg) { return GSQL_ERROR; }
