/*
**  $Id: g_sqldb.c,v 1.31 1999/10/07 09:10:36 jochen Exp $
**
**  GXSNMP -- An snmp management application
**
**  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
**  (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
**  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.,  59 Temple Place - Suite 330, Cambridge, MA 02139, USA.
**
**  Subroutines for maintaining an in-storage, SQL-backed keyed database.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <glib.h>
#include <string.h>
#include <g_sql.h>
#include <g_sqldb.h>

#include "debug.h"

/******************************************************************************
**
**  Forward declarations
**
******************************************************************************/

static void          table_setup	    (G_sqldb_table * table);

static void          register_row 	    (G_sqldb_table * table,
               				     gchar         * row);

static G_sql_query * connect_select_query   (G_sqldb_table * table,
                      			     gchar         * query);

static void 	     disconnect 	    (G_sqldb_table * table);

static void          issue_callbacks        (G_sqldb_table * table, 
					     gpointer        row, 
					     gint 	     type);

/******************************************************************************
**
**  Definition of the private data block for each row in the table.
**  This structure is chained off of each row, via a pointer located at
**  offset (row_private) bytes into the row structure.
**
******************************************************************************/

typedef struct _G_sqldb_row_private
{
  G_sqldb_table	    * table;	   /* Table this row belongs to */
  gpointer	      key[1];	   /* Array of pointers to copies of keys */
}
G_sqldb_row_private;

/******************************************************************************
**
**  Definition of the private data block for each column in the table.
**  This structure is chained off of the private field in G_sqldb_column
**
******************************************************************************/

typedef struct _G_sqldb_column_private
{
  G_sql_accelerator   accelerator;  /* Accelerator for faster G_sql access */
  GHashTable	    * hash;	    /* Pointer to hash table for column */
  gint		      key_index;    /* Index of column key in key table */
  gint		      highest; 	    /* Highest value of field */
}
G_sqldb_column_private;

/******************************************************************************
**
**  Definition of the private data block for the table as a whole.
**  This structure is chained off of the private field in G_sqldb_table
**
******************************************************************************/

typedef struct _G_sqldb_table_private
{
  G_sql_connection * connection;    /* SQL engine connection block */
  GList		   * glist;	    /* GList of all rows in table */
  gint		     keys;	    /* Total number of hash-keyed columns */
  GHookList	   * hooks;	    /* Holds list of callback hooks */
}
G_sqldb_table_private;

/******************************************************************************
**
**  Definition of an SQL callback hook.
**
******************************************************************************/

typedef struct _G_sqldb_hook
{
  GHook	    hook;		/* The underlying hook structure */
  gint	    type;		/* Event mask for issuing this callback */
  gpointer  row;		/* Row this hook applies to, or NULL for any */
  gpointer  data;		/* User data to supply to callback */
}
G_sqldb_hook;


/******************************************************************************
**
**  Internal function to initialize a G_sqldb_table structure.
**
**  Allocates and initializes:
**
**  o  the table private area
**  o  each column's private area
**  o  a hash table for each keyed column
**
**  Returns TRUE if everything succeeded, FALSE otherwise.
**
******************************************************************************/

static void
table_setup (G_sqldb_table * table)
{
  G_sqldb_column    * column;
  D_FUNC_START;
  g_return_if_fail (table != NULL);
  table->private             = g_new (G_sqldb_table_private, 1);
  if (table->private)
    {
      table->private->connection     = NULL;
      table->private->glist          = NULL;
      table->private->keys 	     = 0;
      table->private->hooks	     = NULL;
      
      column = table->columns;
      while (column->name)
	{
	  column->private = g_new0 (G_sqldb_column_private, 1);
	  if (!(column->private))
	    {
	      g_warning ("table_setup: memory allocation for columns failed"
			 " expect trouble.");
	      return;
	    }
	  if (column->type & G_SQL_KEY)
	    {
	      column->private->key_index = table->private->keys++;
	      switch (column->type & G_SQL_TYPE_MASK)
		{
		case G_SQL_INT:
		case G_SQL_UINT:
		  column->private->hash = g_hash_table_new (g_int_hash,
							    g_int_equal);
		  break;
		  
		case G_SQL_STRING:
		case G_SQL_BLOB:
		  column->private->hash = g_hash_table_new (g_str_hash,
							    g_str_equal);
		  break;
		  
		default:
		  g_warning ("g_sqldb: Invalid column type %d in %s table\n", 
			   column->type, table->table);
		  g_assert_not_reached ();
		}
	    }
	  column++;
	}
    }
  else
    {
      g_warning ("table_setup: table allocation failed expect trouble.\n");
    }
  D_FUNC_END;
}

/******************************************************************************
**
**  Internal subroutine to make and return a copy of a key
**
******************************************************************************/

static gpointer
copy_key (G_sqldb_column * column, 
	  gchar		 * row)
{
  gpointer datap;        /* Pointer to original column data */
  gpointer keyp;         /* Pointer to copy of column data */

  D_FUNC_START;
  g_return_val_if_fail (column != NULL, NULL);
  g_return_val_if_fail (row != NULL, NULL);
  datap = G_STRUCT_MEMBER_P (row, column->column_offset);
  keyp  = NULL;

  switch (column->type & G_SQL_TYPE_MASK)
    {
      case G_SQL_INT:
        keyp = g_new (gint, 1);
        *(gint *)keyp = *(gint *)datap;
	d_print (DEBUG_DUMP, "Key (gint) %d\n", *(gint *)datap);
        break;

      case G_SQL_UINT:
        keyp = g_new (guint, 1);
        *(guint *)keyp = *(guint *)datap;
	d_print (DEBUG_DUMP, "Key (guint) %d\n", *(guint *)datap);
        break;

      case G_SQL_DOUBLE:
        keyp = g_new (gdouble, 1);
        *(gdouble *)keyp = *(gdouble *)datap;
	d_print (DEBUG_DUMP, "Key (double) %f\n", *(gdouble *)datap);
        break;

      case G_SQL_STRING:
        keyp = g_strdup (*(char **)datap);
	d_print (DEBUG_DUMP, "Key (string) %s\n", *(gchar **)datap);
        break;

      default:
        g_assert_not_reached ();
    }
  D_FUNC_END;
  return keyp;
}

/******************************************************************************
**
**  Internal subroutine to determine whether a key column has been changed
**
******************************************************************************/

static gboolean
key_changed (G_sqldb_table  * table,
	     G_sqldb_column * column,
             gchar          * row)
{
  gpointer 		datap;		/* Pointer to original column data */
  gpointer 		keyp;		/* Pointer to copy of column data */
  G_sqldb_row_private * row_private;	/* Pointer to row private data area */

  D_FUNC_START;
  g_return_val_if_fail (row != NULL, FALSE);
  g_return_val_if_fail (table != NULL, FALSE);
  g_return_val_if_fail (column != NULL, FALSE);
  row_private = G_STRUCT_MEMBER (G_sqldb_row_private *, row, 
				 table->row_private);
  datap = G_STRUCT_MEMBER_P (row, column->column_offset);
  keyp  = row_private->key[column->private->key_index];

  switch (column->type & G_SQL_TYPE_MASK)
    {
      case G_SQL_INT:
	D_FUNC_END;
        return (*(gint *)keyp != *(gint *)datap);

      case G_SQL_UINT:
	D_FUNC_END;
        return (*(guint *)keyp != *(guint *)datap);

      case G_SQL_DOUBLE:
	D_FUNC_END;
        return (*(gdouble *)keyp != *(gdouble *)datap);

      case G_SQL_STRING:
	D_FUNC_END;
        return (strcmp ((gchar *)keyp, *(gchar **)datap) != 0);

      default:
        g_assert_not_reached ();
    }  
  D_FUNC_END;
  return FALSE;  /* Not reached */
}

/******************************************************************************
**
**  Internal subroutine to print a column value into a string
**
******************************************************************************/

static gchar * 
print_key (G_sqldb_column * column,
           gchar          * row)
{
  gpointer   datap;     /* Pointer to original column data */
  gint	     escapes;	/* Number of single quotes requiring escapement */
  gchar	   * s1, * s2;	/* Used to scale strings */
  gchar	   * esc;	/* Holds fully escaped string */

  D_FUNC_START;
  g_return_val_if_fail (row != NULL, NULL);
  g_return_val_if_fail (column != NULL, NULL);
  datap = G_STRUCT_MEMBER_P (row, column->column_offset);
  
  switch (column->type & G_SQL_TYPE_MASK)
    {
      case G_SQL_INT:
	D_FUNC_END;
	if ((column->type & G_SQL_FKEY) && (!*(gint *)datap))
	  return g_strdup ("NULL");
	else
          return g_strdup_printf ("%d", *(gint *)datap);

      case G_SQL_UINT:
	D_FUNC_END;
	if ((column->type & G_SQL_FKEY) && (!*(guint *)datap))
	  return g_strdup ("NULL");
	else
          return g_strdup_printf ("%u", *(guint *)datap);

      case G_SQL_DOUBLE:
	D_FUNC_END;
        return g_strdup_printf ("%g", *(gdouble *)datap);

      case G_SQL_STRING:
        if (*(gchar **)datap == NULL)
         {
	  D_FUNC_END;
	  return g_strdup ("NULL");
         }
	d_print (DEBUG_TRACE, " Escaping string.\n");
        escapes = 0;			/* Each single quote must be */
	s1 = *(gchar **)datap;		/* escaped with another single quote */
        while (*s1 != '\0')		/* Count the number of single quotes */
          escapes += (*(s1++) == '\'');
	s1 = *(gchar **)datap;
       esc = g_malloc (strlen(s1) + escapes + 3);
	s2 = esc;
	*(s2++) = '\'';		/* Starting quote */
	while (*s1)
	  {
	    if (*s1 == '\'')	/* For each imbedded quote */
	      *(s2++) = '\'';	/* escape it with another quote */
            *(s2++) = *(s1++);  /* Copy source to destination */
	  }
        *(s2++) = '\'';		/* Ending quote */
	*(s2++) = '\0'; 	/* Zero ends string */
	D_FUNC_END;
        return esc;

      default:
         g_assert_not_reached ();
    }
  D_FUNC_END;
  return NULL;	/* Not reached */
}

/******************************************************************************
**
**  Internal subroutine to turn a GList of string fragments into one long
**  string.
**
******************************************************************************/ 

static gchar *
build_string_from_fragments (GList * gl_frag)
{
  int len;
  GList * gl;
  gchar * result;
  D_FUNC_START;
  g_return_val_if_fail (gl_frag != NULL, NULL);
  len = 0;
  gl = gl_frag;
  while (gl)
    {
      len += strlen (gl->data);
      gl = gl->next;
    }

  result = g_malloc0 (len + 1);
  if (!(result))
    {
      g_warning ("build_string_from_fragments: result memory allocation "
		 "failed. Aborting.\n");
      return NULL;
    }
  gl = gl_frag;
  while (gl)
    {
      result = strcat(result, gl->data);
      g_free (gl->data);
      gl = gl->next;
    }
  g_list_free (gl);
  gl_frag = NULL;
  D_FUNC_END;
  return result;
}

/******************************************************************************
**
**  Internal subroutine to register a new row in the table indices.
**
**  o  Allocate a private area for the row and chain it off of the user-
**     provided pointer in the row.
**  o  For each keyed column, make a copy of the key, and store it in the 
**     private area, then add the key to the appropriate hash table.
**  o  Add the row to the master GList.
**
******************************************************************************/

static void
register_row  (G_sqldb_table * table,
               gchar         * row)
{
  G_sqldb_row_private * row_private;  /* Pointer to private area for row */
  G_sqldb_column      * column;       /* Used to scale the column list */
  gpointer		datap;	      /* Pointer to column data */
  gpointer	       	keyp;	      /* Pointer to key copy of column data */
  D_FUNC_START;
  g_return_if_fail (table != NULL);
  g_return_if_fail (row != NULL);
  row_private = g_malloc0 (sizeof (G_sqldb_row_private) +
                           sizeof (gpointer) * (table->private->keys - 1));

  row_private->table = table;
  G_STRUCT_MEMBER (G_sqldb_row_private *, row, table->row_private) = row_private;

  column = table->columns;
  while (column->name)
    {
      if (column->type & G_SQL_KEY)
	{
	  d_print (DEBUG_DUMP, "Copy key for column %s\n", column->name);
	  keyp = copy_key (column, row);
          g_assert (keyp);
          if (g_hash_table_lookup (column->private->hash, keyp))
            {
              g_warning ("g_sqldb: Attempted to register row in '%s' table "
			 "with duplicate '%s' column value entry. Value '%s'\n",
			 table->table, column->name, print_key (column, row));
              g_assert_not_reached ();
            }
          g_hash_table_insert (column->private->hash, keyp, row);
          row_private->key[column->private->key_index] = keyp;
        }

      if (column->type & G_SQL_HIGHEST)
	if ((column->type & G_SQL_TYPE_MASK) == G_SQL_INT)
	  {
	    datap = G_STRUCT_MEMBER_P (row, column->column_offset);
	    if (*(gint *)datap > column->private->highest)
	      column->private->highest = *(gint *)datap;
	  }

      column++;
    }
  D_FUNC_END;
  table->private->glist = g_list_append (table->private->glist, row);
}

/******************************************************************************
**
**  Internal subroutine to unregister a row from the table indices.
**
**  o  Allocate a private area for the row and chain it off of the user-
**     provided pointer in the row.
**  o  For each keyed column, make a copy of the key, and store it in the
**     private area, then add the key to the appropriate hash table.
**  o  Add the row to the master GList.
**
******************************************************************************/

static void
unregister_row  (G_sqldb_table * table,
                 gchar         * row)
{
  G_sqldb_row_private * row_private;  /* Pointer to private area for row */
  G_sqldb_column      * column;       /* Used to scale the column list */
  gpointer              keyp;         /* Pointer to key copy of column data */
  D_FUNC_START;
  g_return_if_fail (row != NULL);
  g_return_if_fail (table != NULL);
  row_private = G_STRUCT_MEMBER (G_sqldb_row_private *, row, 
				 table->row_private);

  column = table->columns;
  while (column->name)
    {
      if (column->type & G_SQL_KEY)
        {
	  keyp = row_private->key[column->private->key_index];

          if (!g_hash_table_lookup (column->private->hash, keyp))
            {
              g_warning ("g_sqldb: Could not delete row from %s table -- "
			 "Key missing from hash table for column %s\n",
			 table->table, column->name);
              g_assert_not_reached ();
            }
          g_hash_table_remove (column->private->hash, keyp);
	  g_free (keyp);
        }
      column++;
    }
  g_free (row_private);
  table->private->glist = g_list_remove (table->private->glist, row);
  D_FUNC_END;
}

/******************************************************************************
**
**  Internal subroutine to update a row in the table indices.
**
**  Compare each keyed column to the saved copy of the key.  
**  If it is different, then:
**
**  o  Remove the old key from the hash table for the column
**  o  free the old key
**  o  Store a copy of the new key in the key table
**  o  Add the copy of the new key to the hash table for the column
**
******************************************************************************/

static void
update_row  (G_sqldb_table * table,
             gchar         * row)
{
  G_sqldb_row_private * row_private;  /* Pointer to private area for row */
  G_sqldb_column      * column;       /* Used to scale the column list */
  gpointer              datap;        /* Pointer to original column data */
  gpointer              keyp;         /* Pointer to key copy of column data */

  D_FUNC_START;
  g_return_if_fail (row != NULL);
  g_return_if_fail (table != NULL);
  row_private = G_STRUCT_MEMBER (G_sqldb_row_private *, row, 
				 table->row_private);
  column = table->columns;
  g_return_if_fail (column != NULL);
  while (column->name)
    {
      if (column->type & G_SQL_KEY)
        {
	  if (key_changed (table, column, row))
	    {
	      datap = G_STRUCT_MEMBER_P (row, column->column_offset);
	      keyp  = row_private->key[column->private->key_index];

              if (!g_hash_table_lookup (column->private->hash, keyp))
		{
                  g_warning ("g_sqldb: Could not update record in %s table -- "
			     "old key missing from hash table for %s column",
			     table->table, column->name);
		  
                  g_assert_not_reached ();
                }
	      g_hash_table_remove (column->private->hash, keyp);
	      g_free (keyp);
	      keyp = copy_key (column, row);
              g_hash_table_insert (column->private->hash, keyp, row);
            }
	}

      if (column->type & G_SQL_HIGHEST)
        if ((column->type & G_SQL_TYPE_MASK) == G_SQL_INT)
          {
            datap = G_STRUCT_MEMBER_P (row, column->column_offset);
            if (*(gint *)datap > column->private->highest) 
                column->private->highest = *(gint *)datap;
          }

      column++;
    }
  D_FUNC_END;
}

/******************************************************************************
**
**  Internal subroutine to connect to an SQL engine, select a database, and
**  issue a query
**
**  Returns open query handle if successful, NULL on failure.
**
******************************************************************************/

static G_sql_query *  
connect_select_query (G_sqldb_table * table,
		      gchar	    * query)
{

  G_sql_query * dbq;

  D_FUNC_START;
  g_return_val_if_fail (table != NULL, NULL);
  g_return_val_if_fail (table->engine != NULL, NULL);
  d_print (DEBUG_DUMP, " database = %s, user = %s\n", table->table, 
	   table->engine->user);
  if (!table->private->connection)
    table->private->connection = g_sql_connect (table->engine);
  if (!table->private->connection)
    {
      DPRT ("connect_select_query: connection to the db failed, "
	    "returning NULL (expect problems)");
      return NULL;
    }
  DPRT1 ("connect_select_query: selecting table ", *(table->database));
  if (!g_sql_select (table->private->connection, *(table->database)))
    {
      DPRT ("connect_select_query: table connect failed. disconnecting "
	    "and return NULL");
      disconnect (table);
      return NULL;
    }
  d_print (DEBUG_DUMP, " issue query: %s\n", query);
  dbq = g_sql_query (table->private->connection, query, strlen(query));
  if (!dbq)
    {
      d_print (DEBUG_TRACE, " query failed, returning NULL pointer.");
      disconnect (table);
      D_FUNC_END;
      return NULL;
    }
  D_FUNC_END;
  return dbq;
}

/******************************************************************************
**
**  Internal subroutine to disconnect from an SQL engine.
**
******************************************************************************/

static void
disconnect (G_sqldb_table * table)
{
  D_FUNC_START;
  g_return_if_fail (table != NULL);
  if (table->private->connection)
    g_sql_disconnect (table->private->connection);
  table->private->connection = NULL;
  D_FUNC_END;
}

/*******************************************************************************
**
**  g_sqldb_table_load
**
**  Load a database table into storage
**
**  Return Values:
**
**  TRUE  --  The table was successfully loaded
**  FALSE --  A problem occurred, and the table was not loaded.
**
*******************************************************************************/

gboolean
g_sqldb_table_load (G_sqldb_table * table)
{
  gchar		    * query;		/* Text of SQL query */
  G_sql_query       * dbq;  		/* Handle to database query result */

  gchar               * row;          /* Pointer to the new data row */
  G_sqldb_column      * column;       /* Used to scale the column list */
  gpointer              datap;        /* Pointer to place to store data */
  G_sql_accelerator   * accelerator;  /* Used to accelerate G_sql access */
  D_FUNC_START;
  table_setup (table);		
  g_return_val_if_fail (table != NULL, FALSE);
  query = g_strdup_printf ("SELECT * FROM %s", table->table);  
  dbq = connect_select_query (table, query);
  g_free (query);

  if (!dbq)
    {
      disconnect (table);
      return FALSE;
    }

  /* All the accelerator fields are initialized to zero by table_setup */

  while ((g_sql_next_row (dbq))) 	/* Read in each row */
    {
      row = g_malloc0 (table->row_length);
      if (!(row))
	{
	  g_warning ("table_load: memory allocation failed. aborting.\n");
	  return FALSE;
	}
      column = table->columns;
      while (column->name)
        {
          datap       = G_STRUCT_MEMBER_P (row, column->column_offset);
          accelerator = &column->private->accelerator;
          switch (column->type & G_SQL_TYPE_MASK)
            {
              case G_SQL_INT:
              case G_SQL_UINT:
                g_sql_field_int (dbq, accelerator, column->name, datap);
                break;

              case G_SQL_DOUBLE:
                g_sql_field_double (dbq, accelerator, column->name, datap);
                break;

              case G_SQL_STRING:
                g_sql_field_string (dbq, accelerator, column->name, datap);
                break;

              default:
	        g_warning ("g_sqldb: Column %s has unknown data type %d\n",
			 column->name, column->type & G_SQL_TYPE_MASK);
                g_assert_not_reached ();
            }
          column++;
        }
      register_row  (table, row);
    }

  g_sql_free_query (dbq);		/* Finished with query */ 
  disconnect (table);     		/* Finished with database */
  D_FUNC_END;
  return TRUE;				/* Table was read in successfully */
}

/******************************************************************************
**
**  g_sqldb_table_list
**
**  Returns a glist of all rows in a table
**
******************************************************************************/

GList *
g_sqldb_table_list (G_sqldb_table * table)
{
  d_print (DEBUG_TRACE, " \n");
  g_return_val_if_fail (table != NULL, NULL);
  return table->private->glist;
}  

/******************************************************************************
**
**  g_sqldb_row_add
**
**  Adds a new row to an existing table, both in memory and in the SQL database
**
******************************************************************************/

gboolean      
g_sqldb_row_add (G_sqldb_table * table, gpointer row)
{
  GList		    * glq;		/* Used to accumulate query text */
  G_sqldb_column    * column;		/* Used to scale the column list */
  gchar             * query;            /* Text of SQL query */
  G_sql_query       * dbq;              /* Handle to database query result */

  D_FUNC_START;
  g_return_val_if_fail (table != NULL, FALSE);
  g_return_val_if_fail (row != NULL, FALSE);
  issue_callbacks (table, row, G_SQLDB_CB_ADD | G_SQLDB_CB_BEFORE);

  register_row (table, row);		/* Add to memory structure first */
/*
**  Construct "INSERT INTO table (column1, column2, ... columnx) VALUES (" 
*/

  glq = g_list_append (NULL, 
		g_strdup_printf ("INSERT INTO %s (", table->table));
  column = table->columns;
  while (column->name)
    {

      glq = g_list_append (glq, (column + 1)->name ?
			        g_strdup_printf ("%s, ", column->name) :
			        g_strdup_printf ("%s) VALUES (", column->name));
      column++;
    }

/*
**  Construct "value1, value2, ... valuex)"
*/
  column = table->columns;
  while (column->name)
    {
      glq = g_list_append (glq, print_key (column, row));
      glq = g_list_append (glq, (column + 1)->name ?
				g_strdup (", ") :
				g_strdup (")"));
      column++;
    }

  query = build_string_from_fragments (glq);
  g_return_val_if_fail (query != NULL, FALSE);
/*
**  Now issue the query to the database
*/
  dbq = connect_select_query (table, query);
  if (query)
    g_free (query);
  if (dbq)
    g_sql_free_query (dbq);

  disconnect (table);     		/* Finished with database */
  issue_callbacks (table, row, G_SQLDB_CB_ADD | G_SQLDB_CB_AFTER);
  D_FUNC_END;
  return TRUE;
}

/******************************************************************************
**
**  g_sqldb_row_update
**
**  Update an existing row, both in memory and in the backing database
**
******************************************************************************/

gboolean
g_sqldb_row_update (G_sqldb_table * table, gpointer row)
{
  GList             * glq;              /* Used to accumulate query text */
  G_sqldb_column    * column;           /* Used to scale the column list */
  G_sqldb_column    * primary = NULL;	/* Key to use for "WHERE key=value" */ 
  gchar             * query;            /* Text of SQL query */
  G_sql_query       * dbq;              /* Handle to database query result */
  D_FUNC_START;
  g_return_val_if_fail (table != NULL, FALSE);
  g_return_val_if_fail (table != NULL, FALSE);
  issue_callbacks (table, row, G_SQLDB_CB_UPDATE | G_SQLDB_CB_BEFORE);

  update_row (table, row);

  glq = g_list_append (NULL,
                g_strdup_printf ("UPDATE %s SET ", table->table));

  column = table->columns;
  while (column->name)
    {
      if (column->type & G_SQL_PRIMARY)
	primary = column;
      else 
	{
          glq = g_list_append (glq, g_strdup_printf("%s=", column->name));
          glq = g_list_append (glq, print_key (column, row));
          glq = g_list_append (glq, g_strdup ((column + 1)->name ?
						", " : " WHERE "));
        }
      column++;
    } 

  if (!primary)
    {
      g_warning ("g_sqldb: Could not update row in %s table because "
		 "table has no primary key\n", table->table);
      g_assert_not_reached ();
    }
  
  glq = g_list_append (glq, g_strdup_printf ("%s=", primary->name));
  glq = g_list_append (glq, print_key (primary, row));
  
  query = build_string_from_fragments (glq);
  g_return_val_if_fail (query != NULL, FALSE);
/*
**  Now issue the query to the database
*/
  dbq = connect_select_query (table, query);
  if (query)
    g_free (query);
  if (dbq)
    g_sql_free_query (dbq);

  disconnect (table);     		/* Finished with database */
  issue_callbacks (table, row, G_SQLDB_CB_UPDATE | G_SQLDB_CB_AFTER);
  D_FUNC_END;
  return TRUE;
}

/******************************************************************************
**
**  g_sqldb_row_delete
**
**  Delete an existing row both from memory and from the backing database
**
******************************************************************************/

gboolean
g_sqldb_row_delete (G_sqldb_table * table, gpointer row)
{
  GList             * glq;              /* Used to accumulate query text */
  G_sqldb_column    * column;           /* Used to scale the column list */
  G_sqldb_column    * primary = NULL;   /* Key to use for "WHERE key=value" */
  gchar             * query;            /* Text of SQL query */
  G_sql_query       * dbq;              /* Handle to database query result */
  D_FUNC_START;
  g_return_val_if_fail (table != NULL, FALSE);
  g_return_val_if_fail (row != NULL, FALSE);
  issue_callbacks (table, row, G_SQLDB_CB_DELETE | G_SQLDB_CB_BEFORE);

  unregister_row (table, row);
  glq = g_list_append (NULL,
                g_strdup_printf ("DELETE FROM %s WHERE ", table->table));

  column = table->columns;
  while (column->name)
    {
      if (column->type & G_SQL_PRIMARY)
	{
	  primary = column;
	  break;
	}
      column++;
    }

  if (!primary)
    {
      g_warning ("g_sqldb: Could not update row in %s table because "
		 "table has no primary key\n", table->table);
      g_assert_not_reached ();
    }

  glq = g_list_append (glq, g_strdup_printf ("%s=", primary->name));
  glq = g_list_append (glq, print_key (primary, row));
  /* glq is freed in this call */
  query = build_string_from_fragments (glq);
  g_return_val_if_fail (query != NULL, FALSE);
/*
**  Now issue the query to the database
*/
  dbq = connect_select_query (table, query);
  if (query)
    g_free (query);
  if (dbq)
    g_sql_free_query (dbq);
  query = NULL;
  dbq = NULL;

  disconnect (table);     		/* Finished with database */
  issue_callbacks (table, row, G_SQLDB_CB_DELETE | G_SQLDB_CB_AFTER);
  D_FUNC_END;
  return TRUE;
}

/******************************************************************************
**
**  g_sqldb_row_find
**
**  Search a database for a record
**
**  Returns the record, or NULL if the record was not found.
**
******************************************************************************/

gpointer
g_sqldb_row_find (G_sqldb_table * table, gchar * field, gpointer keyp)
{
  G_sqldb_column      * column;       /* Used to scale the column list */
  D_FUNC_START;
  g_return_val_if_fail (field != NULL, NULL);
  g_return_val_if_fail (table != NULL, NULL);
  g_return_val_if_fail (keyp != NULL, NULL);
  column = table->columns;
  while (column->name)
    {
      if (!strcmp(column->name, field))
        break;
      column++;
    }
  if (!column->name) 
    {
      g_warning ("g_sqldb: Column %s does not exist in table %s\n",
		 field, table->table);
      D_FUNC_END;
      return NULL;
    }
  D_FUNC_END;
  return g_hash_table_lookup (column->private->hash, keyp);
}
/****************************************************************************
 * g_sqldb_reload_row:
 *
 * Will re-read the specified row from the database. (In case another client
 * has changed the datum.
 ***************************************************************************/
gboolean
g_sqldb_reload_row (G_sqldb_table *table, gpointer row)
{
  GList             *glq;
  G_sqldb_column    *column;
  gchar             *query;
  G_sql_query       *dbq;
  G_sql_accelerator *accelerator;
  gpointer          datap;

  D_FUNC_START;
  /* Call all the 'update' callbacks as we will be possibly updating this */
  issue_callbacks (table, row, G_SQLDB_CB_UPDATE | G_SQLDB_CB_BEFORE);

  glq = g_list_append (NULL,
                       g_strdup_printf ("SELECT * FROM %s ", table->table));
  column = table->columns;
  while (column->name)
    {
      if (column->type & G_SQL_PRIMARY)
        {
          glq = g_list_append (glq, g_strdup_printf (" WHERE %s=",
                               column->name));
          glq = g_list_append (glq, print_key (column, row));
        }
      column++;
    }
  query = build_string_from_fragments (glq);

  dbq = connect_select_query (table, query);
  g_free (query);
  if (!dbq)
    {
      g_warning ("Query %s failed.\n", query);
      disconnect (table);
      D_FUNC_END;
      return FALSE;
    }
  g_sql_next_row (dbq);    /* we ASSUME only one row returned */
  column = table->columns;
  while (column->name)
    {
      datap       = G_STRUCT_MEMBER_P (row, column->column_offset);
      accelerator = &column->private->accelerator;
      switch (column->type & G_SQL_TYPE_MASK)
        {
        case G_SQL_INT:
        case G_SQL_UINT:
          g_sql_field_int (dbq, accelerator, column->name, datap);
          break;

        case G_SQL_DOUBLE:
          g_sql_field_double (dbq, accelerator, column->name, datap);
          break;

        case G_SQL_STRING:
          g_sql_field_string (dbq, accelerator, column->name, datap);
          break;

        default:
          g_print ("Error -- Column %s has unknown data type %d\n",
                   column->name, column->type & G_SQL_TYPE_MASK);
          g_assert_not_reached ();
        }
      column++;
    }
  update_row (table, row);
  issue_callbacks (table, row, G_SQLDB_CB_UPDATE | G_SQLDB_CB_AFTER);
  D_FUNC_END;
  return TRUE;
}

/******************************************************************************
**
**  g_sqldb_highest_rowid
**
**  Find the highest existing rowid in a column
**
******************************************************************************/

gint
g_sqldb_highest_rowid (G_sqldb_table * table, gchar * field)
{
  G_sqldb_column * column;
  D_FUNC_START;
  g_return_val_if_fail (table != NULL, -1);
  column = table->columns;
  while (column->name)
    {
      if (!strcmp(column->name, field))
        break;
      column++;
    } 
  if (!column->name)
    {
      g_warning ("g_sqldb: Column %s does not exist in table %s\n",
               table->table, field);
      D_FUNC_END;
      return -1;
    }
  d_print (DEBUG_DUMP, " highest rowid == %d\n", column->private->highest);
  D_FUNC_END;
  return column->private->highest;
}

/******************************************************************************
**
**  g_sqldb_table_cb_add
**
**  Add a callback to a table
**
**  Returns a hook_id, which is required by g_sqldb_table_cb_remove().
**
**  If the row is NULL, then this callback applies to all rows in the
**  table.
**
******************************************************************************/

gint
g_sqldb_table_cb_add (G_sqldb_table   * table,
		      gpointer		row,
		      G_sqldb_cb_type   type,
		      G_sqldb_cb        func,
		      gpointer		data)
{
  GHookList    * hook_list;
  G_sqldb_hook * hook;
  D_FUNC_START;
  g_return_val_if_fail (table != NULL, -1);
  hook_list = table->private->hooks;

  if (!hook_list)
    {
      hook_list = table->private->hooks = g_new (GHookList, 1);
      g_hook_list_init (hook_list, sizeof (G_sqldb_hook));
    }

  hook            = (G_sqldb_hook *) g_hook_alloc (hook_list);
  hook->row       = row;
  hook->type      = type;
  hook->hook.func = func;
  hook->data	  = data;

  g_hook_prepend (hook_list, (GHook *)hook);
  D_FUNC_END;
  return hook->hook.hook_id;
}

/******************************************************************************
**
** issue_callbacks
**
**  Internal function to send the callback stream
**
******************************************************************************/

static void
issue_callbacks (G_sqldb_table * table, gpointer row, gint type)
{

  GHookList * hook_list;
  GHook     * hook;
  D_FUNC_START;
  g_return_if_fail (table != NULL);
  g_return_if_fail (row != NULL);
  hook_list = table->private->hooks;
  if (!hook_list)
    return;

  hook = g_hook_first_valid (hook_list, FALSE);
  while (hook)
    {
      G_sqldb_hook * sqldb_hook = (G_sqldb_hook *) hook;
      G_sqldb_cb     func;
      gboolean       was_in_call;

      func = (G_sqldb_cb) hook->func;
    
      was_in_call = G_HOOK_IN_CALL (hook);
      hook->flags |= G_HOOK_FLAG_IN_CALL;

      if (!sqldb_hook->row || (row == sqldb_hook->row))
        if ((type & sqldb_hook->type) == type)
          func (table, row, type, sqldb_hook->data); 

      if (!was_in_call)
        hook->flags &= ~G_HOOK_FLAG_IN_CALL;

      hook = g_hook_next_valid (hook_list, hook, FALSE);
    }
  D_FUNC_END;
}
 
/* EOF */
