SabrehavenServer/src/database.cpp
2019-09-16 20:38:16 +03:00

296 lines
7.4 KiB
C++

/**
* Tibia GIMUD Server - a free and open-source MMORPG server emulator
* Copyright (C) 2019 Sabrehaven and Mark Samman <mark.samman@gmail.com>
*
* 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "otpch.h"
#include "configmanager.h"
#include "database.h"
#include <mysql/errmsg.h>
extern ConfigManager g_config;
Database::~Database()
{
if (handle != nullptr) {
mysql_close(handle);
}
}
bool Database::connect()
{
// connection handle initialization
handle = mysql_init(nullptr);
if (!handle) {
std::cout << std::endl << "Failed to initialize MySQL connection handle." << std::endl;
return false;
}
// automatic reconnect
bool reconnect = true;
mysql_options(handle, MYSQL_OPT_RECONNECT, &reconnect);
// connects to database
if (!mysql_real_connect(handle, g_config.getString(ConfigManager::MYSQL_HOST).c_str(), g_config.getString(ConfigManager::MYSQL_USER).c_str(), g_config.getString(ConfigManager::MYSQL_PASS).c_str(), g_config.getString(ConfigManager::MYSQL_DB).c_str(), g_config.getNumber(ConfigManager::SQL_PORT), g_config.getString(ConfigManager::MYSQL_SOCK).c_str(), 0)) {
std::cout << std::endl << "MySQL Error Message: " << mysql_error(handle) << std::endl;
return false;
}
DBResult_ptr result = storeQuery("SHOW VARIABLES LIKE 'max_allowed_packet'");
if (result) {
maxPacketSize = result->getNumber<uint64_t>("Value");
}
return true;
}
bool Database::beginTransaction()
{
if (!executeQuery("BEGIN")) {
return false;
}
databaseLock.lock();
return true;
}
bool Database::rollback()
{
if (mysql_rollback(handle) != 0) {
std::cout << "[Error - mysql_rollback] Message: " << mysql_error(handle) << std::endl;
databaseLock.unlock();
return false;
}
databaseLock.unlock();
return true;
}
bool Database::commit()
{
if (mysql_commit(handle) != 0) {
std::cout << "[Error - mysql_commit] Message: " << mysql_error(handle) << std::endl;
databaseLock.unlock();
return false;
}
databaseLock.unlock();
return true;
}
bool Database::executeQuery(const std::string& query)
{
bool success = true;
// executes the query
databaseLock.lock();
while (mysql_real_query(handle, query.c_str(), query.length()) != 0) {
std::cout << "[Error - mysql_real_query] Query: " << query.substr(0, 256) << std::endl << "Message: " << mysql_error(handle) << std::endl;
auto error = mysql_errno(handle);
if (error != CR_SERVER_LOST && error != CR_SERVER_GONE_ERROR && error != CR_CONN_HOST_ERROR && error != 1053/*ER_SERVER_SHUTDOWN*/ && error != CR_CONNECTION_ERROR) {
success = false;
break;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
MYSQL_RES* m_res = mysql_store_result(handle);
databaseLock.unlock();
if (m_res) {
mysql_free_result(m_res);
}
return success;
}
DBResult_ptr Database::storeQuery(const std::string& query)
{
databaseLock.lock();
retry:
while (mysql_real_query(handle, query.c_str(), query.length()) != 0) {
std::cout << "[Error - mysql_real_query] Query: " << query << std::endl << "Message: " << mysql_error(handle) << std::endl;
auto error = mysql_errno(handle);
if (error != CR_SERVER_LOST && error != CR_SERVER_GONE_ERROR && error != CR_CONN_HOST_ERROR && error != 1053/*ER_SERVER_SHUTDOWN*/ && error != CR_CONNECTION_ERROR) {
break;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// we should call that every time as someone would call executeQuery('SELECT...')
// as it is described in MySQL manual: "it doesn't hurt" :P
MYSQL_RES* res = mysql_store_result(handle);
if (res == nullptr) {
std::cout << "[Error - mysql_store_result] Query: " << query << std::endl << "Message: " << mysql_error(handle) << std::endl;
auto error = mysql_errno(handle);
if (error != CR_SERVER_LOST && error != CR_SERVER_GONE_ERROR && error != CR_CONN_HOST_ERROR && error != 1053/*ER_SERVER_SHUTDOWN*/ && error != CR_CONNECTION_ERROR) {
databaseLock.unlock();
return nullptr;
}
goto retry;
}
databaseLock.unlock();
// retrieving results of query
DBResult_ptr result = std::make_shared<DBResult>(res);
if (!result->hasNext()) {
return nullptr;
}
return result;
}
std::string Database::escapeString(const std::string& s) const
{
return escapeBlob(s.c_str(), s.length());
}
std::string Database::escapeBlob(const char* s, uint32_t length) const
{
// the worst case is 2n + 1
size_t maxLength = (length * 2) + 1;
std::string escaped;
escaped.reserve(maxLength + 2);
escaped.push_back('\'');
if (length != 0) {
char* output = new char[maxLength];
mysql_real_escape_string(handle, output, s, length);
escaped.append(output);
delete[] output;
}
escaped.push_back('\'');
return escaped;
}
DBResult::DBResult(MYSQL_RES* res)
{
handle = res;
size_t i = 0;
MYSQL_FIELD* field = mysql_fetch_field(handle);
while (field) {
listNames[field->name] = i++;
field = mysql_fetch_field(handle);
}
row = mysql_fetch_row(handle);
}
DBResult::~DBResult()
{
mysql_free_result(handle);
}
std::string DBResult::getString(const std::string& s) const
{
auto it = listNames.find(s);
if (it == listNames.end()) {
std::cout << "[Error - DBResult::getString] Column '" << s << "' does not exist in result set." << std::endl;
return std::string();
}
if (row[it->second] == nullptr) {
return std::string();
}
return std::string(row[it->second]);
}
const char* DBResult::getStream(const std::string& s, unsigned long& size) const
{
auto it = listNames.find(s);
if (it == listNames.end()) {
std::cout << "[Error - DBResult::getStream] Column '" << s << "' doesn't exist in the result set" << std::endl;
size = 0;
return nullptr;
}
if (row[it->second] == nullptr) {
size = 0;
return nullptr;
}
size = mysql_fetch_lengths(handle)[it->second];
return row[it->second];
}
bool DBResult::hasNext() const
{
return row != nullptr;
}
bool DBResult::next()
{
row = mysql_fetch_row(handle);
return row != nullptr;
}
DBInsert::DBInsert(std::string query) : query(std::move(query))
{
this->length = this->query.length();
}
bool DBInsert::addRow(const std::string& row)
{
// adds new row to buffer
const size_t rowLength = row.length();
length += rowLength;
if (length > Database::getInstance()->getMaxPacketSize() && !execute()) {
return false;
}
if (values.empty()) {
values.reserve(rowLength + 2);
values.push_back('(');
values.append(row);
values.push_back(')');
} else {
values.reserve(values.length() + rowLength + 3);
values.push_back(',');
values.push_back('(');
values.append(row);
values.push_back(')');
}
return true;
}
bool DBInsert::addRow(std::ostringstream& row)
{
bool ret = addRow(row.str());
row.str(std::string());
return ret;
}
bool DBInsert::execute()
{
if (values.empty()) {
return true;
}
// executes buffer
bool res = Database::getInstance()->executeQuery(query + values);
values.clear();
length = query.length();
return res;
}