/*
 * gksql_mysql.cxx
 *
 * MySQL driver module for GnuGk
 *
 * Copyright (c) 2004, Michal Zygmuntowicz
 *
 * This work is published under the GNU Public License (GPL)
 * see file COPYING for details.
 * We also explicitely grant the right to link this code
 * with the OpenH323 library.
 *
 * $Log: gksql_mysql.cxx,v $
 * Revision 1.8  2006/04/14 13:56:19  willamowius
 * call failover code merged
 *
 * Revision 1.1.1.1  2005/11/21 20:19:59  willamowius
 *
 *
 * Revision 1.4  2005/11/15 19:52:56  jan
 * Michal v1 (works, but on in routed, not proxy mode)
 *
 * Revision 1.7  2005/04/24 16:39:44  zvision
 * MSVC6.0 compatibility fixed
 *
 * Revision 1.6  2005/01/16 15:22:35  zvision
 * Database Host parameter accepts only one host now
 *
 * Revision 1.5  2004/08/02 10:52:07  zvision
 * Ability to extract column names from a result set
 *
 * Revision 1.4  2004/07/09 22:11:36  zvision
 * SQLAcct module ported from 2.0 branch
 *
 */
#if HAS_MYSQL

#if defined(_WIN32) && (_MSC_VER <= 1200)
#pragma warning(disable:4786) // warning about too long debug symbol off
#pragma warning(disable:4284)
#endif

#ifdef _WIN32
#pragma comment( lib, "libmysql.lib" )
#endif

#include <ptlib.h>
#include <mysql.h>
#include "gksql.h"

/** Class that encapsulates SQL query result for MySQL backend.
	It does not provide any multithread safety, so should be accessed
	from a single thread at time.
*/
class GkMySQLResult : public GkSQLResult
{
public:
	/// Build the result from SELECT type query
	GkMySQLResult(
		/// SELECT type query result
		MYSQL_RES* selectResult
		);

	/// Build the result from INSERT, DELETE or UPDATE query
	GkMySQLResult(
		/// number of rows affected by the query
		long numRowsAffected
		);

	/// Build the empty	result and store query execution error information
	GkMySQLResult(
		/// MySQL specific error code
		unsigned int errorCode,
		/// MySQL specific error message text
		const char* errorMsg
		);
	
	virtual ~GkMySQLResult();
	
	/** @return
	    Backend specific error message, if the query failed.
	*/	
	virtual PString GetErrorMessage();
	
	/** @return
	    Backend specific error code, if the query failed.
	*/	
	virtual long GetErrorCode();
	
	/** @return
	    True if rows can be fetched in random access order, false if
	    rows have to be fethed sequentially and can be retrieved only once.
	*/
	virtual bool HasRandomAccess();

	/** Fetch a single row from the result set. After each row is fetched,
	    cursor position is moved to a next row.
		
	    @return
	    True if the row has been fetched, false if no more rows are available.
	*/
	virtual bool FetchRow(
		/// array to be filled with string representations of the row fields
		PStringArray& result
		);
	virtual bool FetchRow(
		/// array to be filled with string representations of the row fields
		ResultRow& result
		);

	/** @return
	    True if the column at the index #fieldOffset# is NULL in the row 
	    fetched most recently.
	*/
	virtual bool IsNullField(
		/// index of the column to check
		long fieldOffset
		);
			
	/** Fetch a single row from the result set. This function requires
		that the backend supports random row access.
		
	    @return
	    True if the row has been fetched, false if a row at the given offset
		does not exists or SQL backend does not support random row access.
	*/
	virtual bool FetchRow(
		/// array to be filled with string representations of the row fields
		PStringArray& result,
		/// index (0 based) of the row to fetch
		long rowOffset
		);
	virtual bool FetchRow(
		/// array to be filled with string representations of the row fields
		ResultRow& result,
		/// index (0 based) of the row to fetch
		long rowOffset
		);
		
	/** @return
	    True if the column at the index #fieldOffset# is NULL in the row 
	    at the specified index.
	*/
	virtual bool IsNullField(
		/// index of the column to check
		long fieldOffset,
		/// index (0 based) of the row to check
		long rowOffset
		);

private:
	GkMySQLResult();
	GkMySQLResult(const GkMySQLResult&);
	GkMySQLResult& operator=(const GkMySQLResult&);
	
protected:
	/// query result for SELECT type queries, NULL otherwise
	MYSQL_RES* m_sqlResult;
	/// the most recent row returned by fetch operation
	MYSQL_ROW m_sqlRow;
	/// lenghts (bytes) for each field in m_sqlRow result row
	unsigned long* m_sqlRowLengths;
	/// MySQL specific error code (if the query failed)
	unsigned int m_errorCode;
	/// MySQL specific error message text (if the query failed)
	PString m_errorMessage;
};

/// MySQL backend connection implementation.
class GkMySQLConnection : public GkSQLConnection
{
public:
	/// Build a new MySQL connection object
	GkMySQLConnection(
		/// name to use in the log
		const char* name = "MySQL"
		);
	
	virtual ~GkMySQLConnection();

protected:
	class MySQLConnWrapper : public GkSQLConnection::SQLConnWrapper
	{
	public:
		MySQLConnWrapper(
			/// unique identifier for this connection
			int id,
			/// host:port this connection is made to
			const PString& host,
			/// MySQL connection object
			MYSQL* conn
			) : SQLConnWrapper(id, host), m_conn(conn) {}

		virtual ~MySQLConnWrapper();

	private:
		MySQLConnWrapper();
		MySQLConnWrapper(const MySQLConnWrapper&);
		MySQLConnWrapper& operator=(const MySQLConnWrapper&);

	public:
		MYSQL* m_conn;
	};

	/** Create a new SQL connection using parameters stored in this object.
	    When the connection is to be closed, the object is simply deleted
	    using delete operator.
	    
	    @return
	    NULL if database connection could not be established 
	    or an object of MySQLConnWrapper class.
	*/
	virtual SQLConnPtr CreateNewConnection(
		/// unique identifier for this connection
		int id
		);
	
	/** Execute the query using specified SQL connection.

		@return
		Query execution result.
	*/
	virtual GkSQLResult* ExecuteQuery(
		/// SQL connection to use for query execution
		SQLConnPtr conn,
		/// query string
		const char* queryStr,
		/// maximum time (ms) for the query execution, -1 means infinite
		long timeout = -1
		);
		
	/** Escape any special characters in the string, so it can be used in a SQL query.

		@return
		Escaped string.
	*/
	virtual PString EscapeString(
		/// SQL connection to get escaping parameters from
		SQLConnPtr conn,
		/// string to be escaped
		const char* str
		);

private:
	GkMySQLConnection(const GkMySQLConnection&);
	GkMySQLConnection& operator=(const GkMySQLConnection&);
};


GkMySQLResult::GkMySQLResult(
	/// SELECT type query result
	MYSQL_RES* selectResult
	) 
	: GkSQLResult(false), m_sqlResult(selectResult), m_sqlRow(NULL),
	m_sqlRowLengths(NULL), m_errorCode(0)
{
	if (m_sqlResult) {
		m_numRows = (long)mysql_num_rows(m_sqlResult);
		m_numFields = mysql_num_fields(m_sqlResult);
	} else
		m_queryError = true;
		
	m_selectType = true;
}

GkMySQLResult::GkMySQLResult(
	/// number of rows affected by the query
	long numRowsAffected
	) 
	: GkSQLResult(false), m_sqlResult(NULL), m_sqlRow(NULL), 
	m_sqlRowLengths(NULL), m_errorCode(0)
{
	m_numRows = numRowsAffected;
	m_selectType = false;
}
	
GkMySQLResult::GkMySQLResult(
	/// MySQL specific error code
	unsigned int errorCode,
	/// MySQL specific error message text
	const char* errorMsg
	) 
	: GkSQLResult(true), m_sqlResult(NULL), m_sqlRow(NULL),
	m_sqlRowLengths(NULL), m_errorCode(errorCode), m_errorMessage(errorMsg)
{
}

GkMySQLResult::~GkMySQLResult()
{
	if (m_sqlResult)
		mysql_free_result(m_sqlResult);
}

bool GkMySQLResult::HasRandomAccess()
{
	return true;
}

PString GkMySQLResult::GetErrorMessage()
{
	return m_errorMessage;
}
	
long GkMySQLResult::GetErrorCode()
{
	return m_errorCode;
}

bool GkMySQLResult::FetchRow(
	/// array to be filled with string representations of the row fields
	PStringArray& result
	)
{
	if (m_sqlResult == NULL || m_numRows <= 0)
		return false;
	
	m_sqlRow = mysql_fetch_row(m_sqlResult);
	m_sqlRowLengths = mysql_fetch_lengths(m_sqlResult);
	if (m_sqlRow == NULL || m_sqlRowLengths == NULL) {
		m_sqlRow = NULL;
		m_sqlRowLengths = NULL;
		return false;
	}
		
	result.SetSize(m_numFields);
	
	for (PINDEX i = 0; i < m_numFields; i++)
		result[i] = PString(m_sqlRow[i], m_sqlRowLengths[i]);
	
	return true;
}

bool GkMySQLResult::FetchRow(
	/// array to be filled with string representations of the row fields
	ResultRow& result
	)
{
	if (m_sqlResult == NULL || m_numRows <= 0)
		return false;
	
	m_sqlRow = mysql_fetch_row(m_sqlResult);
	m_sqlRowLengths = mysql_fetch_lengths(m_sqlResult);
	MYSQL_FIELD* fields = mysql_fetch_fields(m_sqlResult);
	if (m_sqlRow == NULL || m_sqlRowLengths == NULL || fields == NULL) {
		m_sqlRow = NULL;
		m_sqlRowLengths = NULL;
		return false;
	}
		
	result.resize(m_numFields);
	
	for (PINDEX i = 0; i < m_numFields; i++) {
		result[i].first = PString(m_sqlRow[i], m_sqlRowLengths[i]);
		result[i].second = fields[i].name;
	}
	
	return true;
}

bool GkMySQLResult::IsNullField(
	/// index of the column to check
	long fieldOffset
	)
{
	return m_sqlRow == NULL || fieldOffset < 0 || fieldOffset >= m_numFields
		|| m_sqlRow[fieldOffset] == NULL;
}

bool GkMySQLResult::FetchRow(
	/// array to be filled with string representations of the row fields
	PStringArray& result,
	/// index (0 based) of the row to fetch
	long rowOffset
	)
{
	if (m_sqlResult == NULL || rowOffset < 0 || rowOffset >= m_numRows)
		return false;

	// remember the current cursor position and restore it on exit
	const MYSQL_ROW_OFFSET oldRowOffset = mysql_row_tell(m_sqlResult);
	mysql_data_seek(m_sqlResult, rowOffset);

	MYSQL_ROW row = mysql_fetch_row(m_sqlResult);
	unsigned long* rowLen = mysql_fetch_lengths(m_sqlResult);
	if (row == NULL || rowLen == NULL) {
		mysql_row_seek(m_sqlResult, oldRowOffset);
		return false;
	}
		
	result.SetSize(m_numFields);
	
	for (PINDEX i = 0; i < m_numFields; i++)
		result[i] = PString(row[i], rowLen[i]);
	
	mysql_row_seek(m_sqlResult, oldRowOffset);	
	return true;
}

bool GkMySQLResult::FetchRow(
	/// array to be filled with string representations of the row fields
	ResultRow& result,
	/// index (0 based) of the row to fetch
	long rowOffset
	)
{
	if (m_sqlResult == NULL || rowOffset < 0 || rowOffset >= m_numRows)
		return false;

	// remember the current cursor position and restore it on exit
	const MYSQL_ROW_OFFSET oldRowOffset = mysql_row_tell(m_sqlResult);
	mysql_data_seek(m_sqlResult, rowOffset);

	MYSQL_ROW row = mysql_fetch_row(m_sqlResult);
	unsigned long* rowLen = mysql_fetch_lengths(m_sqlResult);
	MYSQL_FIELD* fields = mysql_fetch_fields(m_sqlResult);
	if (row == NULL || rowLen == NULL || fields == NULL) {
		mysql_row_seek(m_sqlResult, oldRowOffset);
		return false;
	}
		
	result.resize(m_numFields);
	
	for (PINDEX i = 0; i < m_numFields; i++) {
		result[i].first = PString(row[i], rowLen[i]);
		result[i].second = fields[i].name;
	}
	
	mysql_row_seek(m_sqlResult, oldRowOffset);	
	return true;
}

bool GkMySQLResult::IsNullField(
	/// index of the column to check
	long fieldOffset,
	/// index (0 based) of the row to check
	long rowOffset
	)
{
	if (m_sqlResult == NULL || rowOffset < 0 || rowOffset >= m_numRows
		|| fieldOffset < 0 || fieldOffset >= m_numFields)
		return true;

	// preserve current row cursor position	
	const MYSQL_ROW_OFFSET oldRowOffset = mysql_row_tell(m_sqlResult);
	mysql_data_seek(m_sqlResult, rowOffset);
	MYSQL_ROW row = mysql_fetch_row(m_sqlResult);
	mysql_row_seek(m_sqlResult, oldRowOffset);
	
	return row == NULL || row[fieldOffset] == NULL;
}


GkMySQLConnection::GkMySQLConnection(
	/// name to use in the log
	const char* name
	) : GkSQLConnection(name)
{
}
	
GkMySQLConnection::~GkMySQLConnection()
{
}

GkMySQLConnection::MySQLConnWrapper::~MySQLConnWrapper()
{
	mysql_close(m_conn);
}

GkSQLConnection::SQLConnPtr GkMySQLConnection::CreateNewConnection(
	/// unique identifier for this connection
	int id
	)
{
	const unsigned int CONNECT_TIMEOUT = 10000;

	MYSQL* conn = mysql_init(NULL);
	if (conn == NULL) {
		PTRACE(1, GetName() << "\tCannot allocate MySQL connection object (mysql_init failed)");
		return NULL;
	}
	mysql_options(conn, MYSQL_OPT_CONNECT_TIMEOUT, (const char*)&CONNECT_TIMEOUT);

	// connect to the MySQL database, try each host on the list in case of failure
	if (mysql_real_connect(conn, m_host, m_username, 
			m_password.IsEmpty() ? (const char*)NULL : (const char*)m_password,
			m_database, m_port, NULL, 0)) {
		PTRACE(5, GetName() << "\tMySQL connection to " << m_username << '@' << m_host 
			<< '[' << m_database << "] established successfully"
			);
		return new MySQLConnWrapper(id, m_host, conn);
	} else {
		PTRACE(2, GetName() << "\tMySQL connection to " << m_username << '@' << m_host 
			<< '[' << m_database << "] failed (mysql_real_connect failed): " << mysql_error(conn)
			);
	}
	return NULL;
}
	
GkSQLResult* GkMySQLConnection::ExecuteQuery(
	/// SQL connection to use for query execution
	GkSQLConnection::SQLConnPtr conn,
	/// query string
	const char* queryStr,
	/// maximum time (ms) for the query execution, -1 means infinite
	long /*timeout*/
	)
{
	MYSQL* mysqlconn = ((MySQLConnWrapper*)conn)->m_conn;
	
	int result = mysql_real_query(mysqlconn, queryStr, strlen(queryStr));
	if (result)
		return new GkMySQLResult(result, mysql_error(mysqlconn));
	
	MYSQL_RES* queryResult = mysql_store_result(mysqlconn);
	if (queryResult)
		return new GkMySQLResult(queryResult);

	result = mysql_errno(mysqlconn);
	if (result)
		return new GkMySQLResult(result, mysql_error(mysqlconn));

	return new GkMySQLResult((long)mysql_affected_rows(mysqlconn));
}

PString GkMySQLConnection::EscapeString(
	/// SQL connection to get escaping parameters from
	SQLConnPtr conn,
	/// string to be escaped
	const char* str
	)
{
	PString escapedStr;
	const unsigned long numChars = str ? strlen(str) : 0;
	
	if (numChars) {
		MYSQL* mysqlconn = ((MySQLConnWrapper*)conn)->m_conn;
		escapedStr.SetSize(
			mysql_real_escape_string(
				mysqlconn, escapedStr.GetPointer(numChars*2+1), str, numChars
				) + 1
			);
	}
	return escapedStr;
}

namespace {
	GkSQLCreator<GkMySQLConnection> MySQLCreator("MySQL");
}

#endif /* HAS_MYSQL */


syntax highlighted by Code2HTML, v. 0.9.1