/*--------------------------------------------------------------------------- * mySQL Support Efuns. * Original code written 1999 by Mark Daniel Reidel. * *--------------------------------------------------------------------------- * This file holds the efuns interfacing with mySQL. See the file INSTALL * or doc/concepts/mysql for setup instructions. * * tefun: db_connect() * tefun: db_close() * tefun: db_exec() * tefun: db_fetch() * tefun: db_affected_rows() * tefun: db_conv_string() * tefun: db_handles() * *--------------------------------------------------------------------------- */ #include "driver.h" #ifdef USE_MYSQL #include "typedefs.h" #include "my-alloca.h" #include <stddef.h> #include <string.h> #include <mysql/mysql.h> #include <mysql/errmsg.h> #include <mysql/mysql_version.h> #include "pkg-mysql.h" #include "array.h" #include "interpret.h" #include "simulate.h" #include "svalue.h" #include "xalloc.h" /* Macros */ /* Typetests for xefuns/tefuns */ #define TYPE_TEST1(arg1,type1) \ if ((arg1)->type != type1) \ bad_xefun_arg(1, sp); #define TYPE_TEST2(arg1,type1) \ if ((arg1)->type != type1) \ bad_xefun_arg(2, sp); #define TYPE_TEST3(arg1,type1) \ if ((arg1)->type != type1) \ bad_xefun_arg(3, sp); /* Typetests for vararg xefuns/vefuns */ #define TYPE_TESTV1(arg1,type1) \ if ((arg1)->type != type1) \ bad_xefun_vararg(1, sp); #define TYPE_TESTV2(arg1,type1) \ if ((arg1)->type != type1) \ bad_xefun_vararg(2, sp); #define TYPE_TESTV3(arg1,type1) \ if ((arg1)->type != type1) \ bad_xefun_vararg(3, sp); /* Typetests for efuns */ #define E_TYPE_TESTV1(arg1,type1) \ if ((arg1)->type != type1) \ bad_efun_arg(1, -2, sp); #define E_TYPE_TESTV2(arg1,type1) \ if ((arg1)->type != type1) \ bad_efun_arg(2, -2, sp); /*-------------------------------------------------------------------------*/ typedef struct db_dat_s db_dat_t; /*--- struct db_dat_s: SQL connection handle --- */ struct db_dat_s { db_dat_t *next; db_dat_t *prev; MYSQL *mysql_dat; MYSQL_RES *mysql_result; MYSQL_ROW mysql_row; int32 handle; }; static db_dat_t *my_dat; /* List of connection handles, newest first. */ static int32 next_handle = 1; /* Handle to identify mySQL connections. */ static void raise_db_error (db_dat_t *dat) NORETURN; /*-------------------------------------------------------------------------*/ static db_dat_t * allocate_new_dat(void) /* Allocate a free handle to use for a connection to an SQL-server * and return it. The handle is also chained into the global list my_dat * at its beginning. */ { db_dat_t *tmp; if ( !my_dat ) /* The chained list has not been allocated */ { my_dat = pxalloc(sizeof(*my_dat)); if ( !my_dat ) { error("Out of memory.\n"); /* NOTREACHED */ return NULL; } my_dat->prev = NULL; my_dat->next = NULL; my_dat->mysql_dat = NULL; my_dat->mysql_result = NULL; my_dat->handle = next_handle++; return my_dat; } /* The chained list exists */ tmp = my_dat->prev = pxalloc(sizeof(db_dat_t)); if ( !tmp ) { error("Out of memory.\n"); return NULL; } tmp->next = my_dat; /* Put the new handle to the beginning */ tmp->prev = NULL; tmp->mysql_dat = NULL; tmp->mysql_result = NULL; tmp->handle = next_handle++; my_dat = tmp; return my_dat; } /* allocate_new_dat() */ /*-------------------------------------------------------------------------*/ static db_dat_t * find_dat_by_handle (unsigned int i) /* Return the corresponding db_dat_t-structure for the handle with id <i>. * When the structure has been found, it is moved to the beginning * of the chained list as it is VERY likely that the next operation * will also be performed on this handle. * If the handle was not found, NULL is returned. */ { db_dat_t *tmp, *tmp2, *tmp3; unsigned int id; if ( !my_dat ) return NULL; tmp = my_dat; while ( ((id = tmp->handle) != i) && (tmp = tmp->next) ) NOOP; if ( id != i ) // handle NOT found return NULL; /* Put the selected pointer at the beginning */ if ( tmp == my_dat ) return tmp; tmp2 = tmp->prev; tmp3 = tmp->next; tmp2->next = tmp3; if ( tmp3 ) tmp3->prev = tmp2; my_dat->prev = tmp; tmp->prev = NULL; tmp->next = my_dat; /* Point my_dat at the first entry again */ my_dat = tmp; return tmp; } /* find_dat_by_handle() */ /*-------------------------------------------------------------------------*/ static unsigned int remove_dat (db_dat_t *dat) /* Remove the database-handle from memore and the chained list. * Also clean any memory allocated for SQL-use. * The result is the handle of the data-connection that was * closed, or 0 if the handle was not found. */ { db_dat_t *tmp, *tmp2; unsigned int i = 0; if ( !dat ) return 0; /* Close the database connection */ if ( dat->mysql_dat ) { if (dat->mysql_result) { mysql_free_result(dat->mysql_result); dat->mysql_result = NULL; } i = dat->handle; mysql_close(dat->mysql_dat); } /* Unlink the structure from the list */ tmp = dat->prev; tmp2 = dat->next; if ( tmp ) tmp->next = tmp2; if ( tmp2 ) { if ( !tmp ) my_dat = tmp2; tmp2->prev = tmp; } if ( dat == my_dat ) my_dat = NULL; pfree(dat); return i; } /* remove_dat() */ /*-------------------------------------------------------------------------*/ #if 0 /* UNUSED for now */ static unsigned int remove_dat_by_handle (int i) /* Remove the memory for the handle with the number <handle> from * the memory and return the handle. */ { db_dat_t *tmp; tmp = find_dat_by_handle(i); return remove_dat(tmp); } /* remove_dat_by_handle() */ #endif /*-------------------------------------------------------------------------*/ static void raise_db_error (db_dat_t *dat) /* Raise an error according to the last operation on the passed * SQL-connection. The connection is closed. */ { char *tmp; char *err_string; if ( !dat ) { error( "An unknown error occured during the current database-" "operation\n"); /* NOTREACHED */ abort(); } tmp = mysql_error(dat->mysql_dat); err_string = alloca(strlen(tmp) + 2); strcpy(err_string, tmp); strcat(err_string, "\n"); remove_dat(dat); error(err_string); /* NOTREACHED */ abort(); } /* raise_db_error() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_affected_rows (svalue_t *sp) /* TEFUN db_affected_rows() * * int db_affected_rows(int handle) * * Return the number of affected rows of the last SQL-statement that * has been sent to the SQL-server via handle <handle>. * Only useful for DELETE- or UPDATE-operations. */ { db_dat_t *dat; int rows; unsigned int handle; TYPE_TEST1(sp, T_NUMBER) handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) error("Illegal handle for database.\n"); rows = mysql_affected_rows(dat->mysql_dat); free_svalue(sp); /* Well, it's just a number */ put_number(sp, rows); return sp; } /* f_db_affected_rows() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_conv_string (svalue_t *sp) /* TEFUN db_conv_string() * * string db_conv_string(string str) * * Convert the string <str> into a string that is correctly interpretated * for usage as a string in db_exec(), e.g. ' is replaced with \' and so * on. */ { char *s; char *buff; TYPE_TEST1(sp, T_STRING) s = sp->u.string; buff = alloca(strlen(s)*2 +1); if ( !buff ) { error("Out of memory.\n"); /* NOTREACHED */ return sp; } mysql_escape_string(buff, s, strlen(s)); /* Re-allocate the escaped string to its proper length */ s = string_copy(buff); free_string_svalue(sp); put_malloced_string(sp, s); return sp; } /* f_db_conv_string() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_close (svalue_t *sp) /* TEFUN db_close() * * int db_close(int handle) * * Close the server-connection with the handle <handle> * Return the handle-number on success. */ { p_int handle; db_dat_t *dat; TYPE_TEST1(sp, T_NUMBER) handle = sp->u.number; if ( !(dat = find_dat_by_handle((unsigned int)handle)) ) { error("Illegal handle for database.\n"); /* NOTREACHED */ return sp; } handle = (p_int)remove_dat(dat); free_svalue(sp); /* Well, it's just a number */ put_number(sp, handle); return sp; } /* db_close() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_connect (svalue_t *sp, int num_args) /* TEFUN db_connect() * * int db_connect(string database, void|string user, void|string password) * * Connect to the database <database> on the local mySQL-server. * The return-value is the handle for this connection. * If the database does not exist or the server is NOT started, * a runtime-error is raised. * * Use user and password if supplied. */ { char *database, *user, *password; p_int sock; db_dat_t *tmp; switch(num_args) { case 3: TYPE_TESTV1(sp-2, T_STRING); TYPE_TESTV2(sp-1, T_STRING); TYPE_TESTV3(sp, T_STRING); database = sp[-2].u.string; user = sp[-1].u.string; password = sp->u.string; break; case 2: TYPE_TESTV1(sp-1, T_STRING); TYPE_TESTV2(sp, T_STRING); database = sp[-1].u.string; user = sp->u.string; password = 0; break; case 1: TYPE_TESTV1(sp, T_STRING); database = sp->u.string; user = 0; password = 0; break; default: /* Compiler makes sure that we get at least one arg */ error("Too many arguments to db_connect()\n"); /* NOTREACHED */ return NULL; } tmp = allocate_new_dat(); if ( !tmp ) { error("Out of memory.\n"); /* NOTREACHED */ return NULL; } tmp->mysql_dat = mysql_init(0); if ( !tmp->mysql_dat ) { remove_dat(tmp); error("Out of memory.\n"); /* NOTREACHED */ return NULL; } /* Only connections to LOCALHOST are currently possible * I wouldn't dare to implement synchronous DB-access via * TCP (that's something for ERQ wizards :-). */ if ( !mysql_real_connect(tmp->mysql_dat, "localhost", user, password, database, 0, 0, 0)) { raise_db_error(tmp); /* NOTREACHED */ return sp; } switch (num_args) { case 3: free_string_svalue(sp); sp--; case 2: free_string_svalue(sp); sp--; case 1: free_string_svalue(sp); } sock = (signed)tmp->handle; put_number(sp, sock); return sp; } /* f_db_connect() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_error (svalue_t *sp) /* TEFUN db_error() * * string db_error(int handle) * * Return a string describing the error which occured during the last * database transaction. If the last transaction was successful, 0 * is returned. */ { db_dat_t *dat; unsigned int handle; char *errmsg; TYPE_TEST1(sp, T_NUMBER) handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) { error("Illegal handle for database.\n"); /* NOTREACHED */ return sp; } errmsg = mysql_error(dat->mysql_dat); if (errmsg[0] == '\0') { free_svalue(sp); put_number(sp, 0); } else { char *s; free_svalue(sp); s = xalloc(strlen(errmsg)+1); strcpy(s, errmsg); put_malloced_string(sp, s); } return sp; } /* f_db_error() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_exec (svalue_t *sp) /* TEFUN db_exec() * * int db_exec(int handle, string statement) * * Execute the SQL-statement <statement> for the connection <handle> to * the SQL-server. The result is the handle if all went okay. If there * was an error in the statement, 0 is returned. */ { char *s; db_dat_t *dat; unsigned int handle; unsigned int err_no; TYPE_TEST1(sp-1, T_NUMBER) TYPE_TEST2(sp, T_STRING) handle = (unsigned int)sp[-1].u.number; s = sp->u.string; if ( !(dat = find_dat_by_handle(handle)) ) { error("Illegal handle for database.\n"); /* NOTREACHED */ return sp; } if ( dat->mysql_result ) { mysql_free_result(dat->mysql_result); dat->mysql_result = NULL; } if ( mysql_query(dat->mysql_dat, s) ) { /* either a REAL error occured or just an error in the SQL-statement */ err_no = mysql_errno(dat->mysql_dat); if ( (err_no == CR_COMMANDS_OUT_OF_SYNC) || (err_no == CR_SERVER_GONE_ERROR) || (err_no == CR_SERVER_LOST) || (err_no == CR_UNKNOWN_ERROR) ) { /* A REAL error occured */ raise_db_error(dat); return sp; } /* Just an error in the SQL-statement */ free_string_svalue(sp); sp--; free_svalue(sp); /* Only a number */ put_number(sp, 0); return sp; } /* If we used a select-statement, how many columns are returned? */ #if MYSQL_VERSION_ID < 32224 if ( mysql_num_fields(dat->mysql_dat) ) #else if ( mysql_field_count(dat->mysql_dat) ) #endif { /* Try to initiate a row-by-row transfer */ if ( !(dat->mysql_result = mysql_use_result(dat->mysql_dat)) ) { raise_db_error(dat); /* NOTREACHED */ return sp; } } free_string_svalue(sp); sp--; free_svalue(sp); /* Only a number */ put_number(sp, (signed)handle); return sp; } /* f_db_exec() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_fetch (svalue_t *sp) /* TEFUN db_fetch() * * mixed db_fetch(int handle) * * Retrieve _ONE_ line of result of the latest SQL-action to the server * based on the handle <handle>. If not more results are on the server, * 0 is returned. */ { db_dat_t *dat; vector_t *v; int num_cols, i; unsigned int handle; TYPE_TEST1(sp, T_NUMBER) handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) { error("Illegal handle for database.\n"); /* NOTREACHED */ return sp; } if (!dat->mysql_result) { free_svalue(sp); put_number(sp, 0); return sp; } /* Store the (next) row of the result in dat->mysql_row */ dat->mysql_row = mysql_fetch_row(dat->mysql_result); if ( dat->mysql_row == NULL ) { /* No more rows to fetch */ mysql_free_result(dat->mysql_result); dat->mysql_result = NULL; free_svalue(sp); /* It's a number */ put_number(sp, 0); return sp; } /* How many columns does every line contain? */ num_cols = mysql_num_fields(dat->mysql_result); v = allocate_array(num_cols); if (!v) { error("Out of memory.\n"); /* NOTREACHED */ return sp; } for (i = 0; i < num_cols; i++) if (dat->mysql_row[i]) put_malloced_string(v->item+i, string_copy(dat->mysql_row[i])); /* else return 0 for that entry */ free_svalue(sp); /* It's a number */ put_array(sp, v); return sp; } /* f_db_exec() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_handles (svalue_t *sp) /* TEFUN db_handles() * * int *db_handles() * * Returns an array with all open handles to the SQL-server. * As mySQL is most of the time limited to 100 connections, you * should not let this number grow too big. The handles are sorted * in a special order: The last used handle is the first one and * the handle that hasn't been used for the longest time is * the last one. If no handles are open, an empty array is returned. */ { int elems; int i; db_dat_t *tmp; vector_t *v; tmp = my_dat; /* Maybe there's no open connection yet/anymore */ if ( !tmp ) { v = allocate_array(0); if (!v) { error("Out of memory.\n"); /* NOTREACHED */ return sp; } sp++; put_array(sp, v); return sp; } elems = 1; /* Count how many handles there are */ while (NULL != (tmp = tmp->next)) elems++; /* Allocate an array to store all handle-ids */ v = allocate_array(elems); if (!v) { error("Out of memory.\n"); /* NOTREACHED */ return sp; } /* Now browse through all handles again and store their ids */ tmp = my_dat; for (i = 0; i < elems; i++) { put_number(v->item+i, tmp->handle); tmp = tmp->next; } sp++; put_array(sp, v); return sp; } /* f_db_handles() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_insert_id (svalue_t *sp) /* TEFUN db_insert_id() * * int db_insert_id(int handle) * * After inserting a line into a table with an AUTO_INCREMENT field, * this efun can be used to return the (new) value of the AUTO_INCREMENT * field. */ { db_dat_t *dat; my_ulonglong insertid; unsigned int handle; TYPE_TEST1(sp, T_NUMBER) handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) error("Illegal handle for database.\n"); insertid = mysql_insert_id(dat->mysql_dat); free_svalue(sp); /* Well, it's just a number */ put_number(sp, insertid); return sp; } /* f_db_insert_id() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_coldefs (svalue_t *sp) /* TEFUN db_coldefs() * * string * db_coldefs(int handle) * * Return an array with the column names of the current table. * If the database didn't return a result, the result of this efun * is 0. */ { db_dat_t *dat; vector_t *v; int num_fields, i; unsigned int handle; MYSQL_FIELD *fields; TYPE_TEST1(sp, T_NUMBER) handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) { error("Illegal handle for database.\n"); /* NOTREACHED */ return sp; } if (!dat->mysql_result) { free_svalue(sp); put_number(sp, 0); return sp; } num_fields = mysql_num_fields(dat->mysql_result); v = allocate_array(num_fields); if (!v) { error("Out of memory for result array (%d elements).\n", num_fields); /* NOTREACHED */ return sp; } fields = mysql_fetch_fields(dat->mysql_result); for (i = 0; i < num_fields; i++) { put_malloced_string(v->item+i, string_copy(fields[i].name)); } free_svalue(sp); /* It's a number */ put_array(sp, v); return sp; } /* f_db_coldefs() */ #endif /* USE_MYSQL */ /***************************************************************************/