eq2go/old/common/database/database_core.hpp
2025-08-06 19:00:30 -05:00

573 lines
16 KiB
C++

// Copyright (C) 2007 EQ2EMulator Development Team - GPL v3 License
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cstdarg>
#include <cerrno>
#include <mysql.h>
#include <errmsg.h>
#include "database_result.hpp"
#include "../types.hpp"
#include "../misc_functions.hpp"
#include "../log.hpp"
using namespace std;
// Database configuration file names based on build target
#ifdef LOGIN
#define DB_INI_FILE "login_db.ini"
#elif defined WORLD
#define DB_INI_FILE "world_db.ini"
#elif defined PARSER
#define DB_INI_FILE "parser_db.ini"
#elif defined PATCHER
#define DB_INI_FILE "patcher_db.ini"
#endif
// Initial buffer size for query allocation
#define QUERY_INITIAL_SIZE 512
// Core database class providing MySQL connection management and query execution
class DatabaseCore
{
public:
// Database connection status enumeration
enum eStatus { Closed, Connected, Error };
// Constructor - initializes MySQL connection and member variables
DatabaseCore()
{
mysql_init(&mysql);
int timeout = 10;
mysql_options(&mysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout);
pHost = nullptr;
pPort = 0;
pUser = nullptr;
pPassword = nullptr;
pDatabase = nullptr;
pCompress = false;
pSSL = false;
pStatus = Closed;
MMysql.SetName("DatabaseCore::mysql");
}
// Destructor - closes connection and frees allocated memory
virtual ~DatabaseCore()
{
pStatus = Closed;
mysql_close(&mysql);
#if MYSQL_VERSION_ID >= 50003
mysql_library_end();
#else
mysql_server_end();
#endif
safe_delete_array(pHost);
safe_delete_array(pUser);
safe_delete_array(pPassword);
safe_delete_array(pDatabase);
}
// Get current database connection status
eStatus GetStatus() { return pStatus; }
// Get MySQL error number from last operation
unsigned int GetError() { return mysql_errno(&mysql); }
// Get MySQL error message from last operation
const char* GetErrorMsg() { return mysql_error(&mysql); }
// Connect to database using configuration file settings
bool Connect()
{
char line[256], *key, *val;
char host[256], user[64], password[64], database[64], port[64];
bool found_section = false;
FILE* f;
if ((f = fopen(DB_INI_FILE, "r")) == NULL) {
LogWrite(DATABASE__ERROR, 0, "Database", "Unable to read %s\n", DB_INI_FILE);
return false;
}
memset(host, 0, sizeof(host));
memset(user, 0, sizeof(user));
memset(password, 0, sizeof(password));
memset(database, 0, sizeof(database));
memset(port, 0, sizeof(port));
while (fgets(line, sizeof(line), f) != NULL) {
if (line[0] == '#' || line[0] == '\n' || line[0] == '\r')
continue;
if (!found_section) {
if (strncasecmp(line, "[Database]", 10) == 0)
found_section = true;
}
else {
if ((key = strtok(line, "=")) != NULL) {
if ((val = strtok(NULL, "\r\n")) != NULL) {
if (strncasecmp(line, "host", 4) == 0)
strncpy(host, val, sizeof(host) - 1);
else if (strncasecmp(line, "user", 4) == 0)
strncpy(user, val, sizeof(user) - 1);
else if (strncasecmp(line, "password", 8) == 0)
strncpy(password, val, sizeof(password) - 1);
else if (strncasecmp(line, "database", 8) == 0)
strncpy(database, val, sizeof(database) - 1);
else if (strncasecmp(line, "port", 4) == 0)
strncpy(port, val, sizeof(port) - 1);
}
}
}
}
fclose(f);
if (host[0] == '\0') {
LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'host' in '%s'\n", DB_INI_FILE);
return false;
}
if (user[0] == '\0') {
LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'user' in '%s'\n", DB_INI_FILE);
return false;
}
if (password[0] == '\0') {
LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'password' in '%s'\n", DB_INI_FILE);
return false;
}
if (database[0] == '\0') {
LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'database' in '%s'\n", DB_INI_FILE);
return false;
}
unsigned int portnum = atoul(port);
return Connect(host, user, password, database, portnum);
}
// Connect to database with explicit connection parameters
bool Connect(const char* host, const char* user, const char* password, const char* database, unsigned int port = 3306)
{
LockMutex lock(&MMysql);
safe_delete_array(pHost);
safe_delete_array(pUser);
safe_delete_array(pPassword);
safe_delete_array(pDatabase);
pHost = new char[strlen(host) + 1];
strcpy(pHost, host);
pUser = new char[strlen(user) + 1];
strcpy(pUser, user);
pPassword = new char[strlen(password) + 1];
strcpy(pPassword, password);
pDatabase = new char[strlen(database) + 1];
strcpy(pDatabase, database);
pPort = port;
if (mysql_real_connect(&mysql, pHost, pUser, pPassword, pDatabase, pPort, NULL, CLIENT_FOUND_ROWS) == NULL) {
LogWrite(DATABASE__ERROR, 0, "Database", "Unable to connect to MySQL server at %s:%u: %s\n", host, port, mysql_error(&mysql));
pStatus = Error;
return false;
}
pStatus = Connected;
return true;
}
// Comprehensive query execution method used by Query class and async operations
bool RunQuery(const char* query, int32 querylen, char* errbuf = 0, MYSQL_RES** result = 0, int32* affected_rows = 0, int32* last_insert_id = 0, int32* errnum = 0, bool retry = true)
{
if (errnum)
*errnum = 0;
if (errbuf)
errbuf[0] = 0;
bool ret = false;
LockMutex lock(&MMysql);
if (pStatus != Connected)
Connect();
LogWrite(DATABASE__QUERY, 0, "Database", query);
if (mysql_real_query(&mysql, query, querylen)) {
if (mysql_errno(&mysql) == CR_SERVER_GONE_ERROR)
pStatus = Error;
if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) {
if (retry) {
LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover...");
ret = RunQuery(query, querylen, errbuf, result, affected_rows, last_insert_id, errnum, false);
}
else {
pStatus = Error;
if (errnum)
*errnum = mysql_errno(&mysql);
if (errbuf)
snprintf(errbuf, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(&mysql), mysql_error(&mysql));
LogWrite(DATABASE__ERROR, 0, "Database", "#%i: %s\nQuery:\n%s", mysql_errno(&mysql), mysql_error(&mysql), query);
ret = false;
}
}
else {
if (errnum)
*errnum = mysql_errno(&mysql);
if (errbuf)
snprintf(errbuf, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(&mysql), mysql_error(&mysql));
LogWrite(DATABASE__ERROR, 0, "Database", "#%i: %s\nQuery:\n%s", mysql_errno(&mysql), mysql_error(&mysql), query);
ret = false;
}
}
else {
if (result && mysql_field_count(&mysql)) {
*result = mysql_store_result(&mysql);
}
else if (result)
*result = 0;
if (affected_rows)
*affected_rows = mysql_affected_rows(&mysql);
if (last_insert_id)
*last_insert_id = mysql_insert_id(&mysql);
if (result) {
if (*result) {
ret = true;
}
else {
if (errnum)
*errnum = UINT_MAX;
if (errbuf) {
if ((!affected_rows || (affected_rows && *affected_rows == 0)) && (!last_insert_id || (last_insert_id && *last_insert_id == 0)))
LogWrite(DATABASE__RESULT, 1, "Database", "No Result.");
}
ret = false;
}
}
else {
ret = true;
}
}
if (ret) {
char tmp1[200] = { 0 };
char tmp2[200] = { 0 };
if (result && (*result))
snprintf(tmp1, sizeof(tmp1), ", %i rows returned", (int)mysql_num_rows(*result));
if (affected_rows)
snprintf(tmp2, sizeof(tmp2), ", %i rows affected", (*affected_rows));
LogWrite(DATABASE__DEBUG, 0, "Database", "Query Successful%s%s", tmp1, tmp2);
}
else
LogWrite(DATABASE__DEBUG, 0, "Database", "Query returned no results in %s!\n%s", __FUNCTION__, query);
return ret;
}
// Execute non-SELECT query with printf-style formatting and automatic reconnection
bool Query(const char* query, ...)
{
char* buf;
size_t size = QUERY_INITIAL_SIZE;
int num_chars;
va_list args;
bool ret = true;
LockMutex lock(&MMysql);
while (true) {
if ((buf = (char*)malloc(size)) == NULL) {
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate database query of %u bytes\n", size);
return false;
}
va_start(args, query);
num_chars = vsnprintf(buf, size, query, args);
va_end(args);
if (num_chars > -1 && (size_t)num_chars < size)
break;
if (num_chars > -1)
size = num_chars + 1;
else
size *= 2;
free(buf);
}
if (pStatus != Connected)
Connect();
LogWrite(DATABASE__QUERY, 0, "Database", query);
if (mysql_real_query(&mysql, buf, num_chars) != 0) {
if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) {
LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover and retry query...");
Connect();
if (mysql_real_query(&mysql, buf, num_chars) != 0) {
ret = false;
}
}
else if (!IsIgnoredErrno(mysql_errno(&mysql))) {
LogWrite(DATABASE__ERROR, 0, "Database", "Error %i running MySQL query: %s\n%s\n", mysql_errno(&mysql), mysql_error(&mysql), buf);
ret = false;
}
}
free(buf);
return ret;
}
// Execute SELECT query and store results in DatabaseResult object
bool Select(DatabaseResult* result, const char* query, ...)
{
char* buf;
size_t size = QUERY_INITIAL_SIZE;
int num_chars;
va_list args;
MYSQL_RES* res;
bool ret = true;
LockMutex lock(&MMysql);
while (true) {
if ((buf = (char*)malloc(size)) == NULL) {
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate database query of %u bytes\n", size);
return false;
}
va_start(args, query);
num_chars = vsnprintf(buf, size, query, args);
va_end(args);
if (num_chars > -1 && (size_t)num_chars < size)
break;
if (num_chars > -1)
size = num_chars + 1;
else
size *= 2;
free(buf);
}
if (pStatus != Connected)
Connect();
if (mysql_real_query(&mysql, buf, (unsigned long)num_chars) != 0) {
if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) {
LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover and retry query...");
mysql_close(&mysql);
Connect();
if (mysql_real_query(&mysql, buf, (unsigned long)num_chars) != 0) {
ret = false;
}
}
else if (!IsIgnoredErrno(mysql_errno(&mysql))) {
LogWrite(DATABASE__ERROR, 0, "Database", "Error %i running MySQL query: %s\n%s\n", mysql_errno(&mysql), mysql_error(&mysql), buf);
ret = false;
}
}
if (ret && !IsIgnoredErrno(mysql_errno(&mysql))) {
res = mysql_store_result(&mysql);
if (res != NULL) {
uint8 num_rows = mysql_affected_rows(&mysql);
uint8 num_fields = mysql_field_count(&mysql);
ret = result->StoreResult(res, num_fields, num_rows);
}
else {
LogWrite(DATABASE__ERROR, 0, "Database", "Error storing MySql query result (%d): %s\n%s", mysql_errno(&mysql), mysql_error(&mysql), buf);
ret = false;
}
}
free(buf);
return ret;
}
// Get the auto-increment ID from the last INSERT operation
int32 LastInsertID()
{
return (int32)mysql_insert_id(&mysql);
}
// Get number of rows affected by the last operation
long AffectedRows()
{
return mysql_affected_rows(&mysql);
}
// Escape string for safe SQL usage - caller must free() returned pointer
char* Escape(const char* str, size_t len)
{
char* buf = (char*)malloc(len * 2 + 1);
if (buf == NULL) {
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", len * 2 + 1, __FUNCTION__, __LINE__);
return NULL;
}
mysql_real_escape_string(&mysql, buf, str, len);
return buf;
}
// Escape null-terminated string for safe SQL usage - caller must free() returned pointer
char* Escape(const char* str)
{
return Escape(str, strlen(str));
}
// Escape string and return as std::string - no manual memory management required
string EscapeStr(const char* str, size_t len)
{
char* buf = (char*)malloc(len * 2 + 1);
string ret;
if (buf == NULL) {
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", len * 2 + 1, __FUNCTION__, __LINE__);
return ret;
}
mysql_real_escape_string(&mysql, buf, str, len);
ret.append(buf);
free(buf);
return ret;
}
// Escape null-terminated string and return as std::string
string EscapeStr(const char* str)
{
return EscapeStr(str, strlen(str));
}
// Escape std::string and return as std::string
string EscapeStr(string str)
{
return EscapeStr(str.c_str(), str.length());
}
// Legacy function for compatibility - use EscapeStr instead
string getSafeEscapeString(const char* from_string)
{
return EscapeStr(from_string);
}
// Execute multiple SQL statements from a file with proper result processing
bool QueriesFromFile(const char* file)
{
bool success = true;
long size;
char* buf;
int ret;
MYSQL_RES* res;
FILE* f;
f = fopen(file, "rb");
if (f == NULL) {
LogWrite(DATABASE__ERROR, 0, "Database", "Unable to open '%s' for reading: %s", file, strerror(errno));
return false;
}
fseek(f, 0, SEEK_END);
size = ftell(f);
fseek(f, 0, SEEK_SET);
buf = (char*)malloc(size + 1);
if (buf == NULL) {
fclose(f);
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", size + 1, __FUNCTION__, __LINE__);
return false;
}
if (fread(buf, sizeof(*buf), size, f) != (size_t)size) {
LogWrite(DATABASE__ERROR, 0, "Database", "Failed to read from '%s': %s", file, strerror(errno));
fclose(f);
free(buf);
return false;
}
buf[size] = '\0';
fclose(f);
mysql_set_server_option(&mysql, MYSQL_OPTION_MULTI_STATEMENTS_ON);
ret = mysql_real_query(&mysql, buf, size);
free(buf);
if (ret != 0) {
LogWrite(DATABASE__ERROR, 0, "Database", "Error running MySQL queries from file '%s' (%d): %s", file, mysql_errno(&mysql), mysql_error(&mysql));
success = false;
}
else {
do {
res = mysql_store_result(&mysql);
if (res != NULL)
mysql_free_result(res);
ret = mysql_next_result(&mysql);
if (ret > 0) {
LogWrite(DATABASE__ERROR, 0, "Database", "Error running MySQL queries from file '%s' (%d): %s", file, mysql_errno(&mysql), mysql_error(&mysql));
success = false;
}
} while (ret == 0);
}
mysql_set_server_option(&mysql, MYSQL_OPTION_MULTI_STATEMENTS_OFF);
return success;
}
// Add MySQL error number to ignored list for suppressing specific error logging
void SetIgnoredErrno(unsigned int db_errno)
{
for (auto& errno_val : ignored_errnos) {
if (errno_val == db_errno)
return;
}
ignored_errnos.push_back(db_errno);
}
// Remove MySQL error number from ignored list
void RemoveIgnoredErrno(unsigned int db_errno)
{
for (auto itr = ignored_errnos.begin(); itr != ignored_errnos.end(); itr++) {
if ((*itr) == db_errno) {
ignored_errnos.erase(itr);
break;
}
}
}
// Check if MySQL error number should be ignored for logging
bool IsIgnoredErrno(unsigned int db_errno)
{
for (auto& errno_val : ignored_errnos) {
if (errno_val == db_errno)
return true;
}
return false;
}
// Send keepalive ping to MySQL server and handle connection errors
void Ping()
{
if (!MMysql.trylock()) {
return; // If locked, someone's using it and it doesn't need a keepalive
}
mysql_ping(&mysql);
int32 errnum = mysql_errno(&mysql);
switch (errnum) {
case CR_COMMANDS_OUT_OF_SYNC:
case CR_SERVER_GONE_ERROR:
case CR_UNKNOWN_ERROR: {
LogWrite(DATABASE__ERROR, 0, "Database", "[Database] We lost connection to the database., errno: %i", errnum);
break;
}
}
MMysql.unlock();
}
protected:
MYSQL mysql; // MySQL connection handle
Mutex MMysql; // Thread safety mutex for database operations
eStatus pStatus; // Current connection status
char* pHost; // Database host address
char* pUser; // Database username
char* pPassword; // Database password
char* pDatabase; // Database name
bool pCompress; // Enable compression flag
int32 pPort; // Database port number
bool pSSL; // Enable SSL flag
vector<unsigned int> ignored_errnos; // List of MySQL error numbers to ignore in logging
};