/*--------------------------------------------------------------------------- * 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. * * efun: db_connect() * efun: db_close() * efun: db_exec() * efun: db_fetch() * efun: db_affected_rows() * efun: db_conv_string() * efun: db_handles() * *--------------------------------------------------------------------------- */ #include "driver.h" #ifdef USE_MYSQL #include "typedefs.h" #include "my-alloca.h" #include #include #include #include #include #include #include "pkg-mysql.h" #include "array.h" #include "interpret.h" #include "instrs.h" #include "main.h" #include "mstrings.h" #include "simulate.h" #include "stdstrings.h" #include "svalue.h" #include "xalloc.h" /*-------------------------------------------------------------------------*/ 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; /*-------------------------------------------------------------------------*/ Bool pkg_mysql_init (void) /* Initialize the mySQL package and return TRUE on success. * * All there really is to do is to check if the driver was linked against the * correct mysqlclient library. */ { const char * client_version = mysql_get_client_info(); const char * server_version = MYSQL_SERVER_VERSION #ifdef MYSQL_SERVER_SUFFIX MYSQL_SERVER_SUFFIX #endif /* MYSQL_SERVER_SUFFIX */ ; long cl_version, s_version; cl_version = strtol(client_version, NULL, 10); s_version = strtol(server_version, NULL, 10); if (cl_version != s_version) { #ifdef VERBOSE printf("%s %s: mySQL: compiled for %s, linked with %s client.\n" , time_stamp() , cl_version > s_version ? "Fatal" : "Warning" , server_version, client_version); #endif debug_message("%s %s: mySQL: compiled for %s, linked with %s " "client.\n" , time_stamp() , cl_version < s_version ? "Fatal" : "Warning" , server_version, client_version); if (cl_version > s_version) return MY_FALSE; } #ifdef VERBOSE printf("%s mySQL %s\n", time_stamp(), client_version); #endif debug_message("%s mySQL %s\n", time_stamp(), client_version); return MY_TRUE; } /* pkg_mysql_init() */ /*-------------------------------------------------------------------------*/ static Bool check_privilege (const char * efun_name, Bool raise_error, svalue_t * sp) /* Check if the user has the privileges to execute efun . * The function executes a call to master->privilege_violation("mysql", * efun_name) and evaluates the result. * If the master result is TRUE, the function returns TRUE. * If the master result is FALSE, the function returns FALSE if * is FALSE, and raises an error if is true. */ { Bool rc; inter_sp = sp+1; put_c_string(inter_sp, efun_name); rc = privilege_violation(STR_MYSQL, inter_sp, inter_sp); free_svalue(inter_sp); inter_sp--; if (rc) return MY_TRUE; if (raise_error) { errorf("%s(): Privilege violation.\n", efun_name); /* NOTREACHED */ } return MY_FALSE; } /* check_privilege() */ /*-------------------------------------------------------------------------*/ 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 ) { errorf("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 ) { errorf("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 . * 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 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. */ { const char *tmp; char *err_string; if ( !dat ) { errorf( "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); errorf(err_string); /* NOTREACHED */ abort(); } /* raise_db_error() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_affected_rows (svalue_t *sp) /* EFUN 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 . * Only useful for DELETE- or UPDATE-operations. */ { db_dat_t *dat; int rows; unsigned int handle; check_privilege(instrs[F_DB_AFFECTED_ROWS].name, MY_TRUE, sp); handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) errorf("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) /* EFUN db_conv_string() * * string db_conv_string(string str) * * Convert the string into a string that is correctly interpretated * for usage as a string in db_exec(), e.g. ' is replaced with \' and so * on. */ { string_t *s; char *buff; s = sp->u.str; buff = xalloc(mstrsize(s)*2 +1); if ( !buff ) { errorf("Out of memory (%zu bytes) in db_conv_string().\n", mstrsize(s)*2 + 1); /* NOTREACHED */ return sp; } mysql_escape_string(buff, get_txt(s), strlen(get_txt(s)) ); xfree(buff); free_string_svalue(sp); put_c_string(sp, buff); return sp; } /* f_db_conv_string() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_close (svalue_t *sp) /* EFUN db_close() * * int db_close(int handle) * * Close the server-connection with the handle * Return the handle-number on success. */ { p_int handle; db_dat_t *dat; check_privilege(instrs[F_DB_CLOSE].name, MY_TRUE, sp); handle = sp->u.number; if ( !(dat = find_dat_by_handle((unsigned int)handle)) ) { errorf("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 * v_db_connect (svalue_t *sp, int num_args) /* EFUN db_connect() * * int db_connect(string database, void|string user, void|string password) * * Connect to the 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. */ { string_t *database, *user, *password; p_int sock; db_dat_t *tmp; check_privilege(instrs[F_DB_CONNECT].name, MY_TRUE, sp); switch(num_args) { case 3: database = sp[-2].u.str; user = sp[-1].u.str; password = sp->u.str; break; case 2: database = sp[-1].u.str; user = sp->u.str; password = NULL; break; default: database = sp->u.str; user = NULL; password = NULL; break; } tmp = allocate_new_dat(); if ( !tmp ) { errorf("Out of memory.\n"); /* NOTREACHED */ return NULL; } tmp->mysql_dat = mysql_init(0); if ( !tmp->mysql_dat ) { remove_dat(tmp); errorf("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 ? get_txt(user) : NULL , password ? get_txt(password) : NULL , get_txt(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; } /* v_db_connect() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_error (svalue_t *sp) /* EFUN 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; const char *errmsg; check_privilege(instrs[F_DB_ERROR].name, MY_TRUE, sp); handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) { errorf("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 { free_svalue(sp); put_c_string(sp, errmsg); } return sp; } /* f_db_error() */ /*-------------------------------------------------------------------------*/ svalue_t * f_db_exec (svalue_t *sp) /* EFUN db_exec() * * int db_exec(int handle, string statement) * * Execute the SQL-statement for the connection to * the SQL-server. The result is the handle if all went okay. If there * was an error in the statement, 0 is returned. */ { string_t *s; db_dat_t *dat; unsigned int err_no; unsigned int handle; handle = (unsigned int)sp[-1].u.number; s = sp->u.str; check_privilege(instrs[F_DB_EXEC].name, MY_TRUE, sp); if ( !(dat = find_dat_by_handle(handle)) ) { errorf("Illegal handle for database.\n"); /* NOTREACHED */ return sp; } if ( dat->mysql_result ) { mysql_free_result(dat->mysql_result); dat->mysql_result = NULL; } if ( mysql_real_query(dat->mysql_dat, get_txt(s), mstrsize(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) /* EFUN db_fetch() * * mixed db_fetch(int handle) * * Retrieve _ONE_ line of result of the latest SQL-action to the server * based on the 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; check_privilege(instrs[F_DB_FETCH].name, MY_TRUE, sp); handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) { errorf("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) { errorf("Out of memory.\n"); /* NOTREACHED */ return sp; } for (i = 0; i < num_cols; i++) if (dat->mysql_row[i]) put_c_string(v->item+i, 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) /* EFUN 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; check_privilege(instrs[F_DB_HANDLES].name, MY_TRUE, sp); tmp = my_dat; /* Maybe there's no open connection yet/anymore */ if ( !tmp ) { v = allocate_array(0); if (!v) { errorf("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) { errorf("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; check_privilege(instrs[F_DB_INSERT_ID].name, MY_TRUE, sp); handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) errorf("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; check_privilege(instrs[F_DB_COLDEFS].name, MY_TRUE, sp); handle = (unsigned int)sp->u.number; if ( !(dat = find_dat_by_handle(handle)) ) { errorf("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) { errorf("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_c_string(v->item+i, fields[i].name); } free_svalue(sp); /* It's a number */ put_array(sp, v); return sp; } /* f_db_coldefs() */ #endif /* USE_MYSQL */ /***************************************************************************/