diff options
Diffstat (limited to 'xbmc/dbwrappers/mysqldataset.cpp')
-rw-r--r-- | xbmc/dbwrappers/mysqldataset.cpp | 2099 |
1 files changed, 2099 insertions, 0 deletions
diff --git a/xbmc/dbwrappers/mysqldataset.cpp b/xbmc/dbwrappers/mysqldataset.cpp new file mode 100644 index 0000000..a1a2d9d --- /dev/null +++ b/xbmc/dbwrappers/mysqldataset.cpp @@ -0,0 +1,2099 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "mysqldataset.h" + +#include "Util.h" +#include "network/DNSNameCache.h" +#include "network/WakeOnAccess.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <array> +#include <iostream> +#include <set> +#include <string> +#ifdef HAS_MYSQL +#include <mysql/errmsg.h> +#elif defined(HAS_MARIADB) +#include <mariadb/errmsg.h> +#endif + +#ifdef TARGET_POSIX +#include "platform/posix/ConvUtils.h" +#endif + +#define MYSQL_OK 0 +#define ER_BAD_DB_ERROR 1049 + +namespace dbiplus +{ + +//************* MysqlDatabase implementation *************** + +MysqlDatabase::MysqlDatabase() +{ + + active = false; + _in_transaction = false; // for transaction + + error = "Unknown database error"; //S_NO_CONNECTION; + host = "localhost"; + port = "3306"; + db = "mysql"; + login = "root"; + passwd = "null"; + conn = NULL; + default_charset = ""; +} + +MysqlDatabase::~MysqlDatabase() +{ + disconnect(); +} + +Dataset* MysqlDatabase::CreateDataset() const +{ + return new MysqlDataset(const_cast<MysqlDatabase*>(this)); +} + +int MysqlDatabase::status(void) +{ + if (active == false) + return DB_CONNECTION_NONE; + return DB_CONNECTION_OK; +} + +int MysqlDatabase::setErr(int err_code, const char* qry) +{ + switch (err_code) + { + case MYSQL_OK: + error = "Successful result"; + break; + case CR_COMMANDS_OUT_OF_SYNC: + error = "Commands were executed in an improper order"; + break; + case CR_SERVER_GONE_ERROR: + error = "The MySQL server has gone away"; + break; + case CR_SERVER_LOST: + error = "The connection to the server was lost during this query"; + break; + case CR_UNKNOWN_ERROR: + error = "An unknown error occurred"; + break; + case 1146: /* ER_NO_SUCH_TABLE */ + error = "The table does not exist"; + break; + default: + char err[256]; + snprintf(err, 256, "Undefined MySQL error: Code (%d)", err_code); + error = err; + } + error = "[" + db + "] " + error; + error += "\nQuery: "; + error += qry; + error += "\n"; + return err_code; +} + +const char* MysqlDatabase::getErrorMsg() +{ + return error.c_str(); +} + +void MysqlDatabase::configure_connection() +{ + char sqlcmd[512]; + int ret; + + // MySQL 5.7.5+: See #8393 + strcpy(sqlcmd, + "SET SESSION sql_mode = (SELECT REPLACE(@@SESSION.sql_mode,'ONLY_FULL_GROUP_BY',''))"); + if ((ret = mysql_real_query(conn, sqlcmd, strlen(sqlcmd))) != MYSQL_OK) + throw DbErrors("Can't disable sql_mode ONLY_FULL_GROUP_BY: '%s' (%d)", db.c_str(), ret); + + // MySQL 5.7.6+: See #8393. Non-fatal if error, as not supported by MySQL 5.0.x + strcpy(sqlcmd, "SELECT @@SESSION.optimizer_switch"); + if ((ret = mysql_real_query(conn, sqlcmd, strlen(sqlcmd))) == MYSQL_OK) + { + MYSQL_RES* res = mysql_store_result(conn); + MYSQL_ROW row; + + if (res) + { + if ((row = mysql_fetch_row(res)) != NULL) + { + std::string column = row[0]; + std::vector<std::string> split = StringUtils::Split(column, ','); + + for (std::string& itIn : split) + { + if (StringUtils::Trim(itIn) == "derived_merge=on") + { + strcpy(sqlcmd, "SET SESSION optimizer_switch = 'derived_merge=off'"); + if ((ret = mysql_real_query(conn, sqlcmd, strlen(sqlcmd))) != MYSQL_OK) + throw DbErrors("Can't set optimizer_switch = '%s': '%s' (%d)", + StringUtils::Trim(itIn).c_str(), db.c_str(), ret); + break; + } + } + } + mysql_free_result(res); + } + } + else + CLog::Log(LOGWARNING, "Unable to query optimizer_switch: '{}' ({})", db, ret); +} + +int MysqlDatabase::connect(bool create_new) +{ + if (host.empty() || db.empty()) + return DB_CONNECTION_NONE; + + std::string resolvedHost; + if (!StringUtils::EqualsNoCase(host, "localhost") && CDNSNameCache::Lookup(host, resolvedHost)) + { + CLog::Log(LOGDEBUG, "{} replacing configured host {} with resolved host {}", __FUNCTION__, host, + resolvedHost); + host = resolvedHost; + } + + try + { + disconnect(); + + if (conn == NULL) + { + conn = mysql_init(conn); + mysql_ssl_set(conn, key.empty() ? NULL : key.c_str(), cert.empty() ? NULL : cert.c_str(), + ca.empty() ? NULL : ca.c_str(), capath.empty() ? NULL : capath.c_str(), + ciphers.empty() ? NULL : ciphers.c_str()); + } + + if (!CWakeOnAccess::GetInstance().WakeUpHost(host, "MySQL : " + db)) + return DB_CONNECTION_NONE; + + // establish connection with just user credentials + if (mysql_real_connect(conn, host.c_str(), login.c_str(), passwd.c_str(), NULL, + atoi(port.c_str()), NULL, compression ? CLIENT_COMPRESS : 0) != NULL) + { + static bool showed_ver_info = false; + if (!showed_ver_info) + { + std::string version_string = mysql_get_server_info(conn); + CLog::Log(LOGINFO, "MYSQL: Connected to version {}", version_string); + showed_ver_info = true; + unsigned long version = mysql_get_server_version(conn); + // Minimum for MySQL: 5.6 (5.5 is EOL) + unsigned long min_version = 50600; + if (version_string.find("MariaDB") != std::string::npos) + { + // Minimum for MariaDB: 5.5 (still supported) + min_version = 50500; + } + + if (version < min_version) + { + CLog::Log( + LOGWARNING, + "MYSQL: Your database server version {} is very old and might not be supported in " + "future Kodi versions. Please consider upgrading to MySQL 5.7 or MariaDB 10.2.", + version_string); + } + } + + // disable mysql autocommit since we handle it + //mysql_autocommit(conn, false); + + // enforce utf8 charset usage + default_charset = mysql_character_set_name(conn); + if (mysql_set_character_set(conn, "utf8")) // returns 0 on success + { + CLog::Log(LOGERROR, "Unable to set utf8 charset: {} [{}]({})", db, mysql_errno(conn), + mysql_error(conn)); + } + + configure_connection(); + + // check existence + if (exists()) + { + // nothing to see here + } + else if (create_new) + { + char sqlcmd[512]; + int ret; + + snprintf(sqlcmd, sizeof(sqlcmd), + "CREATE DATABASE `%s` CHARACTER SET utf8 COLLATE utf8_general_ci", db.c_str()); + if ((ret = query_with_reconnect(sqlcmd)) != MYSQL_OK) + { + throw DbErrors("Can't create new database: '%s' (%d)", db.c_str(), ret); + } + } + + if (mysql_select_db(conn, db.c_str()) == 0) + { + active = true; + return DB_CONNECTION_OK; + } + } + + // if we failed above, either credentials were incorrect or the database didn't exist + if (mysql_errno(conn) == ER_BAD_DB_ERROR && create_new) + { + + if (create() == MYSQL_OK) + { + active = true; + + return DB_CONNECTION_OK; + } + } + + CLog::Log(LOGERROR, "Unable to open database: {} [{}]({})", db, mysql_errno(conn), + mysql_error(conn)); + + return DB_CONNECTION_NONE; + } + catch (...) + { + CLog::Log(LOGERROR, "Unable to open database: {} ({})", db, GetLastError()); + } + return DB_CONNECTION_NONE; +} + +void MysqlDatabase::disconnect(void) +{ + if (conn != NULL) + { + mysql_close(conn); + conn = NULL; + } + + active = false; +} + +int MysqlDatabase::create() +{ + return connect(true); +} + +int MysqlDatabase::drop() +{ + if (!active) + throw DbErrors("Can't drop database: no active connection..."); + char sqlcmd[512]; + int ret; + snprintf(sqlcmd, sizeof(sqlcmd), "DROP DATABASE `%s`", db.c_str()); + if ((ret = query_with_reconnect(sqlcmd)) != MYSQL_OK) + { + throw DbErrors("Can't drop database: '%s' (%d)", db.c_str(), ret); + } + disconnect(); + return DB_COMMAND_OK; +} + +int MysqlDatabase::copy(const char* backup_name) +{ + if (!active || conn == NULL) + throw DbErrors("Can't copy database: no active connection..."); + + char sql[4096]; + int ret; + + // ensure we're connected to the db we are about to copy + if ((ret = mysql_select_db(conn, db.c_str())) != MYSQL_OK) + throw DbErrors("Can't connect to source database: '%s'", db.c_str()); + + // grab a list of base tables only (no views) + sprintf(sql, "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"); + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + throw DbErrors("Can't determine base tables for copy."); + + // get list of all tables from old DB + MYSQL_RES* res = mysql_store_result(conn); + + if (res) + { + if (mysql_num_rows(res) == 0) + { + mysql_free_result(res); + throw DbErrors("The source database was unexpectedly empty."); + } + + // create the new database + snprintf(sql, sizeof(sql), "CREATE DATABASE `%s` CHARACTER SET utf8 COLLATE utf8_general_ci", + backup_name); + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + { + mysql_free_result(res); + throw DbErrors("Can't create database for copy: '%s' (%d)", db.c_str(), ret); + } + + MYSQL_ROW row; + + // duplicate each table from old db to new db + while ((row = mysql_fetch_row(res)) != NULL) + { + // copy the table definition + snprintf(sql, sizeof(sql), "CREATE TABLE `%s`.%s LIKE %s", backup_name, row[0], row[0]); + + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + { + mysql_free_result(res); + throw DbErrors("Can't copy schema for table '%s'\nError: %d", row[0], ret); + } + + // copy the table data + snprintf(sql, sizeof(sql), "INSERT INTO `%s`.%s SELECT * FROM %s", backup_name, row[0], + row[0]); + + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + { + mysql_free_result(res); + throw DbErrors("Can't copy data for table '%s'\nError: %d", row[0], ret); + } + } + mysql_free_result(res); + + // we don't recreate views, indices, or triggers on copy + // as we'll be dropping and recreating them anyway + } + + return 1; +} + +int MysqlDatabase::drop_analytics(void) +{ + if (!active || conn == NULL) + throw DbErrors("Can't clean database: no active connection..."); + + char sql[4096]; + int ret; + + // ensure we're connected to the db we are about to clean from stuff + if ((ret = mysql_select_db(conn, db.c_str())) != MYSQL_OK) + throw DbErrors("Can't connect to database: '%s'", db.c_str()); + + // getting a list of indexes in the database + snprintf(sql, sizeof(sql), + "SELECT DISTINCT table_name, index_name " + "FROM information_schema.statistics " + "WHERE index_name != 'PRIMARY' AND table_schema = '%s'", + db.c_str()); + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + throw DbErrors("Can't determine list of indexes to drop."); + + // we will acquire lists here + MYSQL_RES* res = mysql_store_result(conn); + MYSQL_ROW row; + + if (res) + { + while ((row = mysql_fetch_row(res)) != NULL) + { + snprintf(sql, sizeof(sql), "ALTER TABLE `%s`.%s DROP INDEX %s", db.c_str(), row[0], row[1]); + + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + { + mysql_free_result(res); + throw DbErrors("Can't drop index '%s'\nError: %d", row[0], ret); + } + } + mysql_free_result(res); + } + + // next topic is a views list + snprintf(sql, sizeof(sql), + "SELECT table_name FROM information_schema.views WHERE table_schema = '%s'", db.c_str()); + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + throw DbErrors("Can't determine list of views to drop."); + + res = mysql_store_result(conn); + + if (res) + { + while ((row = mysql_fetch_row(res)) != NULL) + { + /* we do not need IF EXISTS because these views are exist */ + snprintf(sql, sizeof(sql), "DROP VIEW `%s`.%s", db.c_str(), row[0]); + + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + { + mysql_free_result(res); + throw DbErrors("Can't drop view '%s'\nError: %d", row[0], ret); + } + } + mysql_free_result(res); + } + + // triggers + snprintf(sql, sizeof(sql), + "SELECT trigger_name FROM information_schema.triggers WHERE event_object_schema = '%s'", + db.c_str()); + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + throw DbErrors("Can't determine list of triggers to drop."); + + res = mysql_store_result(conn); + + if (res) + { + while ((row = mysql_fetch_row(res)) != NULL) + { + snprintf(sql, sizeof(sql), "DROP TRIGGER `%s`.%s", db.c_str(), row[0]); + + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + { + mysql_free_result(res); + throw DbErrors("Can't drop trigger '%s'\nError: %d", row[0], ret); + } + } + mysql_free_result(res); + } + + // Native functions + snprintf(sql, sizeof(sql), + "SELECT routine_name FROM information_schema.routines " + "WHERE routine_type = 'FUNCTION' and routine_schema = '%s'", + db.c_str()); + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + throw DbErrors("Can't determine list of routines to drop."); + + res = mysql_store_result(conn); + + if (res) + { + while ((row = mysql_fetch_row(res)) != NULL) + { + snprintf(sql, sizeof(sql), "DROP FUNCTION `%s`.%s", db.c_str(), row[0]); + + if ((ret = query_with_reconnect(sql)) != MYSQL_OK) + { + mysql_free_result(res); + throw DbErrors("Can't drop function '%s'\nError: %d", row[0], ret); + } + } + mysql_free_result(res); + } + + return 1; +} + +int MysqlDatabase::query_with_reconnect(const char* query) +{ + int attempts = 5; + int result; + + // try to reconnect if server is gone + while (((result = mysql_real_query(conn, query, strlen(query))) != MYSQL_OK) && + ((result = mysql_errno(conn)) == CR_SERVER_GONE_ERROR || result == CR_SERVER_LOST) && + (attempts-- > 0)) + { + CLog::Log(LOGINFO, "MYSQL server has gone. Will try {} more attempt(s) to reconnect.", + attempts); + active = false; + connect(true); + } + + return result; +} + +long MysqlDatabase::nextid(const char* sname) +{ + CLog::Log(LOGDEBUG, "MysqlDatabase::nextid for {}", sname); + if (!active) + return DB_UNEXPECTED_RESULT; + const char* seq_table = "sys_seq"; + int id; /*,nrow,ncol;*/ + MYSQL_RES* res; + char sqlcmd[512]; + snprintf(sqlcmd, sizeof(sqlcmd), "SELECT nextid FROM %s WHERE seq_name = '%s'", seq_table, sname); + CLog::Log(LOGDEBUG, "MysqlDatabase::nextid will request"); + if ((last_err = query_with_reconnect(sqlcmd)) != 0) + { + return DB_UNEXPECTED_RESULT; + } + res = mysql_store_result(conn); + if (res) + { + if (mysql_num_rows(res) == 0) + { + id = 1; + snprintf(sqlcmd, sizeof(sqlcmd), "INSERT INTO %s (nextid,seq_name) VALUES (%d,'%s')", + seq_table, id, sname); + mysql_free_result(res); + if ((last_err = query_with_reconnect(sqlcmd)) != 0) + return DB_UNEXPECTED_RESULT; + return id; + } + else + { + id = -1; + snprintf(sqlcmd, sizeof(sqlcmd), "UPDATE %s SET nextid=%d WHERE seq_name = '%s'", seq_table, + id, sname); + mysql_free_result(res); + if ((last_err = query_with_reconnect(sqlcmd)) != 0) + return DB_UNEXPECTED_RESULT; + return id; + } + } + return DB_UNEXPECTED_RESULT; +} + +// methods for transactions +// --------------------------------------------- +void MysqlDatabase::start_transaction() +{ + if (active) + { + mysql_autocommit(conn, false); + CLog::Log(LOGDEBUG, "Mysql Start transaction"); + _in_transaction = true; + } +} + +void MysqlDatabase::commit_transaction() +{ + if (active) + { + mysql_commit(conn); + mysql_autocommit(conn, true); + CLog::Log(LOGDEBUG, "Mysql commit transaction"); + _in_transaction = false; + } +} + +void MysqlDatabase::rollback_transaction() +{ + if (active) + { + mysql_rollback(conn); + mysql_autocommit(conn, true); + CLog::Log(LOGDEBUG, "Mysql rollback transaction"); + _in_transaction = false; + } +} + +bool MysqlDatabase::exists(void) +{ + bool ret = false; + + if (conn == NULL || mysql_ping(conn)) + { + CLog::Log(LOGERROR, "Not connected to database, test of existence is not possible."); + return ret; + } + + MYSQL_RES* result = mysql_list_dbs(conn, db.c_str()); + if (result == NULL) + { + CLog::Log(LOGERROR, "Database is not present, does the user has CREATE DATABASE permission"); + return false; + } + + ret = (mysql_num_rows(result) > 0); + mysql_free_result(result); + + // Check if there is some tables ( to permit user with no create database rights + if (ret) + { + result = mysql_list_tables(conn, NULL); + if (result != NULL) + ret = (mysql_num_rows(result) > 0); + + mysql_free_result(result); + } + + return ret; +} + +// methods for formatting +// --------------------------------------------- +std::string MysqlDatabase::vprepare(const char* format, va_list args) +{ + std::string strFormat = format; + std::string strResult = ""; + size_t pos; + + // %q is the sqlite format string for %s. + // Any bad character, like "'", will be replaced with a proper one + pos = 0; + while ((pos = strFormat.find("%s", pos)) != std::string::npos) + strFormat.replace(pos++, 2, "%q"); + + strResult = mysql_vmprintf(strFormat.c_str(), args); + // RAND() is the mysql form of RANDOM() + pos = 0; + while ((pos = strResult.find("RANDOM()", pos)) != std::string::npos) + { + strResult.replace(pos++, 8, "RAND()"); + pos += 6; + } + + // Replace some dataypes in CAST statements: + // before: CAST(iFoo AS TEXT), CAST(foo AS INTEGER) + // after: CAST(iFoo AS CHAR), CAST(foo AS SIGNED INTEGER) + pos = strResult.find("CAST("); + while (pos != std::string::npos) + { + size_t pos2 = strResult.find(" AS TEXT)", pos + 1); + if (pos2 != std::string::npos) + strResult.replace(pos2, 9, " AS CHAR)"); + else + { + pos2 = strResult.find(" AS INTEGER)", pos + 1); + if (pos2 != std::string::npos) + strResult.replace(pos2, 12, " AS SIGNED INTEGER)"); + } + pos = strResult.find("CAST(", pos + 1); + } + + // Remove COLLATE NOCASE the SQLite case insensitive collation. + // In MySQL all tables are defined with case insensitive collation utf8_general_ci + pos = 0; + while ((pos = strResult.find(" COLLATE NOCASE", pos)) != std::string::npos) + strResult.erase(pos++, 15); + + // Remove COLLATE ALPHANUM the SQLite custom collation. + pos = 0; + while ((pos = strResult.find(" COLLATE ALPHANUM", pos)) != std::string::npos) + strResult.erase(pos++, 15); + + return strResult; +} + +/* vsprintf() functionality is based on sqlite3.c functions */ + +/* +** Conversion types fall into various categories as defined by the +** following enumeration. +*/ +#define etRADIX 1 /* Integer types. %d, %x, %o, and so forth */ +#define etFLOAT 2 /* Floating point. %f */ +#define etEXP 3 /* Exponential notation. %e and %E */ +#define etGENERIC 4 /* Floating or exponential, depending on exponent. %g */ +#define etSIZE 5 /* Return number of characters processed so far. %n */ +#define etSTRING 6 /* Strings. %s */ +#define etDYNSTRING 7 /* Dynamically allocated strings. %z */ +#define etPERCENT 8 /* Percent symbol. %% */ +#define etCHARX 9 /* Characters. %c */ +/* The rest are extensions, not normally found in printf() */ +#define etSQLESCAPE 10 /* Strings with '\'' doubled. Strings with '\\' escaped. %q */ +#define etSQLESCAPE2 \ + 11 /* Strings with '\'' doubled and enclosed in '', + NULL pointers replaced by SQL NULL. %Q */ +#define etPOINTER 14 /* The %p conversion */ +#define etSQLESCAPE3 15 /* %w -> Strings with '\"' doubled */ + +#define etINVALID 0 /* Any unrecognized conversion type */ + +/* +** An "etByte" is an 8-bit unsigned value. +*/ +typedef unsigned char etByte; + +/* +** Each builtin conversion character (ex: the 'd' in "%d") is described +** by an instance of the following structure +*/ +typedef struct et_info +{ /* Information about each format field */ + char fmttype; /* The format field code letter */ + etByte base; /* The base for radix conversion */ + etByte flags; /* One or more of FLAG_ constants below */ + etByte type; /* Conversion paradigm */ + etByte charset; /* Offset into aDigits[] of the digits string */ + etByte prefix; /* Offset into aPrefix[] of the prefix string */ +} et_info; + +/* +** An objected used to accumulate the text of a string where we +** do not necessarily know how big the string will be in the end. +*/ +struct StrAccum +{ + char* zBase; /* A base allocation. Not from malloc. */ + char* zText; /* The string collected so far */ + int nChar; /* Length of the string so far */ + int nAlloc; /* Amount of space allocated in zText */ + int mxAlloc; /* Maximum allowed string length */ + bool mallocFailed; /* Becomes true if any memory allocation fails */ + bool tooBig; /* Becomes true if string size exceeds limits */ +}; + +/* +** Allowed values for et_info.flags +*/ +#define FLAG_SIGNED 1 /* True if the value to convert is signed */ +#define FLAG_INTERN 2 /* True if for internal use only */ +#define FLAG_STRING 4 /* Allow infinity precision */ + +/* +** The following table is searched linearly, so it is good to put the +** most frequently used conversion types first. +*/ +static const char aDigits[] = "0123456789ABCDEF0123456789abcdef"; +static const char aPrefix[] = "-x0\000X0"; +// clang-format off +constexpr std::array<et_info, 20> fmtinfo = {{ + {'d', 10, 1, etRADIX, 0, 0}, + {'s', 0, 4, etSTRING, 0, 0}, + {'g', 0, 1, etGENERIC, 30, 0}, + {'z', 0, 4, etDYNSTRING, 0, 0}, + {'q', 0, 4, etSQLESCAPE, 0, 0}, + {'Q', 0, 4, etSQLESCAPE2, 0, 0}, + {'w', 0, 4, etSQLESCAPE3, 0, 0}, + {'c', 0, 0, etCHARX, 0, 0}, + {'o', 8, 0, etRADIX, 0, 2}, + {'u', 10, 0, etRADIX, 0, 0}, + {'x', 16, 0, etRADIX, 16, 1}, + {'X', 16, 0, etRADIX, 0, 4}, + {'f', 0, 1, etFLOAT, 0, 0}, + {'e', 0, 1, etEXP, 30, 0}, + {'E', 0, 1, etEXP, 14, 0}, + {'G', 0, 1, etGENERIC, 14, 0}, + {'i', 10, 1, etRADIX, 0, 0}, + {'n', 0, 0, etSIZE, 0, 0}, + {'%', 0, 0, etPERCENT, 0, 0}, + {'p', 16, 0, etPOINTER, 0, 1}, +}}; +// clang-format on + +/* +** "*val" is a double such that 0.1 <= *val < 10.0 +** Return the ascii code for the leading digit of *val, then +** multiply "*val" by 10.0 to renormalize. +** +** Example: +** input: *val = 3.14159 +** output: *val = 1.4159 function return = '3' +** +** The counter *cnt is incremented each time. After counter exceeds +** 16 (the number of significant digits in a 64-bit float) '0' is +** always returned. +*/ +char MysqlDatabase::et_getdigit(double* val, int* cnt) +{ + int digit; + double d; + if ((*cnt)++ >= 16) + return '0'; + digit = (int)*val; + d = digit; + digit += '0'; + *val = (*val - d) * 10.0; + return (char)digit; +} + +/* +** Append N space characters to the given string buffer. +*/ +void MysqlDatabase::appendSpace(StrAccum* pAccum, int N) +{ + static const char zSpaces[] = " "; + while (N >= (int)sizeof(zSpaces) - 1) + { + mysqlStrAccumAppend(pAccum, zSpaces, sizeof(zSpaces) - 1); + N -= sizeof(zSpaces) - 1; + } + if (N > 0) + { + mysqlStrAccumAppend(pAccum, zSpaces, N); + } +} + +#ifndef MYSQL_PRINT_BUF_SIZE +#define MYSQL_PRINT_BUF_SIZE 350 +#endif + +#define etBUFSIZE MYSQL_PRINT_BUF_SIZE /* Size of the output buffer */ + +/* +** The maximum length of a TEXT or BLOB in bytes. This also +** limits the size of a row in a table or index. +** +** The hard limit is the ability of a 32-bit signed integer +** to count the size: 2^31-1 or 2147483647. +*/ +#ifndef MYSQL_MAX_LENGTH +#define MYSQL_MAX_LENGTH 1000000000 +#endif + +/* +** The root program. All variations call this core. +** +** INPUTS: +** func This is a pointer to a function taking three arguments +** 1. A pointer to anything. Same as the "arg" parameter. +** 2. A pointer to the list of characters to be output +** (Note, this list is NOT null terminated.) +** 3. An integer number of characters to be output. +** (Note: This number might be zero.) +** +** arg This is the pointer to anything which will be passed as the +** first argument to "func". Use it for whatever you like. +** +** fmt This is the format string, as in the usual print. +** +** ap This is a pointer to a list of arguments. Same as in +** vfprint. +** +** OUTPUTS: +** The return value is the total number of characters sent to +** the function "func". Returns -1 on a error. +** +** Note that the order in which automatic variables are declared below +** seems to make a big difference in determining how fast this beast +** will run. +*/ +void MysqlDatabase::mysqlVXPrintf(StrAccum* pAccum, /* Accumulate results here */ + int useExtended, /* Allow extended %-conversions */ + const char* fmt, /* Format string */ + va_list ap /* arguments */ +) +{ + int c; /* Next character in the format string */ + char* bufpt; /* Pointer to the conversion buffer */ + int precision; /* Precision of the current field */ + int length; /* Length of the field */ + int idx; /* A general purpose loop counter */ + int width; /* Width of the current field */ + etByte flag_leftjustify; /* True if "-" flag is present */ + etByte flag_plussign; /* True if "+" flag is present */ + etByte flag_blanksign; /* True if " " flag is present */ + etByte flag_alternateform; /* True if "#" flag is present */ + etByte flag_altform2; /* True if "!" flag is present */ + etByte flag_zeropad; /* True if field width constant starts with zero */ + etByte flag_long; /* True if "l" flag is present */ + etByte flag_longlong; /* True if the "ll" flag is present */ + etByte done; /* Loop termination flag */ + uint64_t longvalue; /* Value for integer types */ + double realvalue; /* Value for real types */ + const et_info* infop; /* Pointer to the appropriate info structure */ + char buf[etBUFSIZE]; /* Conversion buffer */ + char prefix; /* Prefix character. "+" or "-" or " " or '\0'. */ + etByte xtype = 0; /* Conversion paradigm */ + char* zExtra; /* Extra memory used for etTCLESCAPE conversions */ + int exp, e2; /* exponent of real numbers */ + double rounder; /* Used for rounding floating point values */ + etByte flag_dp; /* True if decimal point should be shown */ + etByte flag_rtz; /* True if trailing zeros should be removed */ + etByte flag_exp; /* True to force display of the exponent */ + int nsd; /* Number of significant digits returned */ + + length = 0; + bufpt = 0; + for (; (c = (*fmt)) != 0; ++fmt) + { + bool isLike = false; + if (c != '%') + { + int amt; + bufpt = const_cast<char*>(fmt); + amt = 1; + while ((c = (*++fmt)) != '%' && c != 0) + amt++; + isLike = mysqlStrAccumAppend(pAccum, bufpt, amt); + if (c == 0) + break; + } + if ((c = (*++fmt)) == 0) + { + mysqlStrAccumAppend(pAccum, "%", 1); + break; + } + /* Find out what flags are present */ + flag_leftjustify = flag_plussign = flag_blanksign = flag_alternateform = flag_altform2 = + flag_zeropad = 0; + done = 0; + do + { + switch (c) + { + case '-': + flag_leftjustify = 1; + break; + case '+': + flag_plussign = 1; + break; + case ' ': + flag_blanksign = 1; + break; + case '#': + flag_alternateform = 1; + break; + case '!': + flag_altform2 = 1; + break; + case '0': + flag_zeropad = 1; + break; + default: + done = 1; + break; + } + } while (!done && (c = (*++fmt)) != 0); + /* Get the field width */ + width = 0; + if (c == '*') + { + width = va_arg(ap, int); + if (width < 0) + { + flag_leftjustify = 1; + width = -width; + } + c = *++fmt; + } + else + { + while (c >= '0' && c <= '9') + { + width = width * 10 + c - '0'; + c = *++fmt; + } + } + if (width > etBUFSIZE - 10) + { + width = etBUFSIZE - 10; + } + /* Get the precision */ + if (c == '.') + { + precision = 0; + c = *++fmt; + if (c == '*') + { + precision = va_arg(ap, int); + if (precision < 0) + precision = -precision; + c = *++fmt; + } + else + { + while (c >= '0' && c <= '9') + { + precision = precision * 10 + c - '0'; + c = *++fmt; + } + } + } + else + { + precision = -1; + } + /* Get the conversion type modifier */ + if (c == 'l') + { + flag_long = 1; + c = *++fmt; + if (c == 'l') + { + flag_longlong = 1; + c = *++fmt; + } + else + { + flag_longlong = 0; + } + } + else + { + flag_long = flag_longlong = 0; + } + /* Fetch the info entry for the field */ + infop = fmtinfo.data(); + xtype = etINVALID; + + for (const auto& info : fmtinfo) + { + if (c != info.fmttype) + continue; + + infop = &info; + + if (useExtended || (infop->flags & FLAG_INTERN) == 0) + { + xtype = infop->type; + } + else + { + return; + } + + break; + } + + zExtra = 0; + + /* Limit the precision to prevent overflowing buf[] during conversion */ + if (precision > etBUFSIZE - 40 && (infop->flags & FLAG_STRING) == 0) + { + precision = etBUFSIZE - 40; + } + + /* + ** At this point, variables are initialized as follows: + ** + ** flag_alternateform TRUE if a '#' is present. + ** flag_altform2 TRUE if a '!' is present. + ** flag_plussign TRUE if a '+' is present. + ** flag_leftjustify TRUE if a '-' is present or if the + ** field width was negative. + ** flag_zeropad TRUE if the width began with 0. + ** flag_long TRUE if the letter 'l' (ell) prefixed + ** the conversion character. + ** flag_longlong TRUE if the letter 'll' (ell ell) prefixed + ** the conversion character. + ** flag_blanksign TRUE if a ' ' is present. + ** width The specified field width. This is + ** always non-negative. Zero is the default. + ** precision The specified precision. The default + ** is -1. + ** xtype The class of the conversion. + ** infop Pointer to the appropriate info struct. + */ + switch (xtype) + { + case etPOINTER: + flag_longlong = sizeof(char*) == sizeof(int64_t); + flag_long = sizeof(char*) == sizeof(long int); + /* Fall through into the next case */ + [[fallthrough]]; + case etRADIX: + if (infop->flags & FLAG_SIGNED) + { + int64_t v; + if (flag_longlong) + { + v = va_arg(ap, int64_t); + } + else if (flag_long) + { + v = va_arg(ap, long int); + } + else + { + v = va_arg(ap, int); + } + if (v < 0) + { + longvalue = -v; + prefix = '-'; + } + else + { + longvalue = v; + if (flag_plussign) + prefix = '+'; + else if (flag_blanksign) + prefix = ' '; + else + prefix = 0; + } + } + else + { + if (flag_longlong) + { + longvalue = va_arg(ap, uint64_t); + } + else if (flag_long) + { + longvalue = va_arg(ap, unsigned long int); + } + else + { + longvalue = va_arg(ap, unsigned int); + } + prefix = 0; + } + if (longvalue == 0) + flag_alternateform = 0; + if (flag_zeropad && precision < width - (prefix != 0)) + { + precision = width - (prefix != 0); + } + bufpt = &buf[etBUFSIZE - 1]; + { + const char* cset; + int base; + cset = &aDigits[infop->charset]; + base = infop->base; + do + { /* Convert to ascii */ + *(--bufpt) = cset[longvalue % base]; + longvalue = longvalue / base; + } while (longvalue > 0); + } + length = (int)(&buf[etBUFSIZE - 1] - bufpt); + for (idx = precision - length; idx > 0; idx--) + { + *(--bufpt) = '0'; /* Zero pad */ + } + if (prefix) + *(--bufpt) = prefix; /* Add sign */ + if (flag_alternateform && infop->prefix) + { /* Add "0" or "0x" */ + const char* pre; + char x; + pre = &aPrefix[infop->prefix]; + for (; (x = (*pre)) != 0; pre++) + *(--bufpt) = x; + } + length = (int)(&buf[etBUFSIZE - 1] - bufpt); + bufpt[length] = 0; + break; + case etFLOAT: + case etEXP: + case etGENERIC: + realvalue = va_arg(ap, double); + if (precision < 0) + precision = 6; /* Set default precision */ + if (precision > etBUFSIZE / 2 - 10) + precision = etBUFSIZE / 2 - 10; + if (realvalue < 0.0) + { + realvalue = -realvalue; + prefix = '-'; + } + else + { + if (flag_plussign) + prefix = '+'; + else if (flag_blanksign) + prefix = ' '; + else + prefix = 0; + } + if (xtype == etGENERIC && precision > 0) + precision--; + /* It makes more sense to use 0.5 */ + for (idx = precision, rounder = 0.5; idx > 0; idx--, rounder *= 0.1) + { + } + if (xtype == etFLOAT) + realvalue += rounder; + /* Normalize realvalue to within 10.0 > realvalue >= 1.0 */ + exp = 0; +#if 0 + if( mysqlIsNaN((double)realvalue) ){ + bufpt = "NaN"; + length = 3; + break; + } +#endif + if (realvalue > 0.0) + { + while (realvalue >= 1e32 && exp <= 350) + { + realvalue *= 1e-32; + exp += 32; + } + while (realvalue >= 1e8 && exp <= 350) + { + realvalue *= 1e-8; + exp += 8; + } + while (realvalue >= 10.0 && exp <= 350) + { + realvalue *= 0.1; + exp++; + } + while (realvalue < 1e-8) + { + realvalue *= 1e8; + exp -= 8; + } + while (realvalue < 1.0) + { + realvalue *= 10.0; + exp--; + } + if (exp > 350) + { + if (prefix == '-') + { + bufpt = const_cast<char*>("-Inf"); + } + else if (prefix == '+') + { + bufpt = const_cast<char*>("+Inf"); + } + else + { + bufpt = const_cast<char*>("Inf"); + } + length = strlen(bufpt); + break; + } + } + bufpt = buf; + /* + ** If the field type is etGENERIC, then convert to either etEXP + ** or etFLOAT, as appropriate. + */ + flag_exp = xtype == etEXP; + if (xtype != etFLOAT) + { + realvalue += rounder; + if (realvalue >= 10.0) + { + realvalue *= 0.1; + exp++; + } + } + if (xtype == etGENERIC) + { + flag_rtz = !flag_alternateform; + if (exp < -4 || exp > precision) + { + xtype = etEXP; + } + else + { + precision = precision - exp; + xtype = etFLOAT; + } + } + else + { + flag_rtz = 0; + } + if (xtype == etEXP) + { + e2 = 0; + } + else + { + e2 = exp; + } + nsd = 0; + flag_dp = (precision > 0 ? 1 : 0) | flag_alternateform | flag_altform2; + /* The sign in front of the number */ + if (prefix) + { + *(bufpt++) = prefix; + } + /* Digits prior to the decimal point */ + if (e2 < 0) + { + *(bufpt++) = '0'; + } + else + { + for (; e2 >= 0; e2--) + { + *(bufpt++) = et_getdigit(&realvalue, &nsd); + } + } + /* The decimal point */ + if (flag_dp) + { + *(bufpt++) = '.'; + } + /* "0" digits after the decimal point but before the first + ** significant digit of the number */ + for (e2++; e2 < 0; precision--, e2++) + { + //ASSERT( precision>0 ); + *(bufpt++) = '0'; + } + /* Significant digits after the decimal point */ + while ((precision--) > 0) + { + *(bufpt++) = et_getdigit(&realvalue, &nsd); + } + /* Remove trailing zeros and the "." if no digits follow the "." */ + if (flag_rtz && flag_dp) + { + while (bufpt[-1] == '0') + *(--bufpt) = 0; + //ASSERT( bufpt>buf ); + if (bufpt[-1] == '.') + { + if (flag_altform2) + { + *(bufpt++) = '0'; + } + else + { + *(--bufpt) = 0; + } + } + } + /* Add the "eNNN" suffix */ + if (flag_exp || xtype == etEXP) + { + *(bufpt++) = aDigits[infop->charset]; + if (exp < 0) + { + *(bufpt++) = '-'; + exp = -exp; + } + else + { + *(bufpt++) = '+'; + } + if (exp >= 100) + { + *(bufpt++) = (char)((exp / 100) + '0'); /* 100's digit */ + exp %= 100; + } + *(bufpt++) = (char)(exp / 10 + '0'); /* 10's digit */ + *(bufpt++) = (char)(exp % 10 + '0'); /* 1's digit */ + } + *bufpt = 0; + + /* The converted number is in buf[] and zero terminated. Output it. + ** Note that the number is in the usual order, not reversed as with + ** integer conversions. */ + length = (int)(bufpt - buf); + bufpt = buf; + + /* Special case: Add leading zeros if the flag_zeropad flag is + ** set and we are not left justified */ + if (flag_zeropad && !flag_leftjustify && length < width) + { + int i; + int nPad = width - length; + for (i = width; i >= nPad; i--) + { + bufpt[i] = bufpt[i - nPad]; + } + i = prefix != 0; + while (nPad--) + bufpt[i++] = '0'; + length = width; + } + break; + case etSIZE: + *(va_arg(ap, int*)) = pAccum->nChar; + length = width = 0; + break; + case etPERCENT: + buf[0] = '%'; + bufpt = buf; + length = 1; + break; + case etCHARX: + c = va_arg(ap, int); + buf[0] = (char)c; + if (precision >= 0) + { + for (idx = 1; idx < precision; idx++) + buf[idx] = (char)c; + length = precision; + } + else + { + length = 1; + } + bufpt = buf; + break; + case etSTRING: + case etDYNSTRING: + bufpt = va_arg(ap, char*); + if (bufpt == 0) + { + bufpt = const_cast<char*>(""); + } + else if (xtype == etDYNSTRING) + { + zExtra = bufpt; + } + if (precision >= 0) + { + for (length = 0; length < precision && bufpt[length]; length++) + { + } + } + else + { + length = strlen(bufpt); + } + break; + case etSQLESCAPE: + case etSQLESCAPE2: + case etSQLESCAPE3: + { + int i, j, k, n, isnull; + int needQuote; + char ch; + char q = ((xtype == etSQLESCAPE3) ? '"' : '\''); /* Quote character */ + std::string arg = va_arg(ap, char*); + if (isLike) + StringUtils::Replace(arg, "\\", "\\\\"); + const char* escarg = arg.c_str(); + + isnull = escarg == 0; + if (isnull) + escarg = (xtype == etSQLESCAPE2 ? "NULL" : "(NULL)"); + k = precision; + for (i = 0; k != 0 && (ch = escarg[i]) != 0; i++, k--) + ; + needQuote = !isnull && xtype == etSQLESCAPE2; + n = i * 2 + 1 + needQuote * 2; + if (n > etBUFSIZE) + { + bufpt = zExtra = (char*)malloc(n); + if (bufpt == 0) + { + pAccum->mallocFailed = true; + return; + } + } + else + { + bufpt = buf; + } + j = 0; + if (needQuote) + bufpt[j++] = q; + k = i; + j += mysql_real_escape_string(conn, bufpt, escarg, k); + if (needQuote) + bufpt[j++] = q; + bufpt[j] = 0; + length = j; + /* The precision in %q and %Q means how many input characters to + ** consume, not the length of the output... + ** if( precision>=0 && precision<length ) length = precision; */ + break; + } + default: + { + return; + } + } /* End switch over the format type */ + /* + ** The text of the conversion is pointed to by "bufpt" and is + ** "length" characters long. The field width is "width". Do + ** the output. + */ + if (!flag_leftjustify) + { + int nspace; + nspace = width - length; + if (nspace > 0) + { + appendSpace(pAccum, nspace); + } + } + if (length > 0) + { + mysqlStrAccumAppend(pAccum, bufpt, length); + } + if (flag_leftjustify) + { + int nspace; + nspace = width - length; + if (nspace > 0) + { + appendSpace(pAccum, nspace); + } + } + if (zExtra) + { + free(zExtra); + } + } /* End for loop over the format string */ +} /* End of function */ + +/* +** Append N bytes of text from z to the StrAccum object. +*/ +bool MysqlDatabase::mysqlStrAccumAppend(StrAccum* p, const char* z, int N) +{ + if (p->tooBig | p->mallocFailed) + { + return false; + } + if (N < 0) + { + N = strlen(z); + } + if (N == 0 || z == 0) + { + return false; + } + if (p->nChar + N >= p->nAlloc) + { + char* zNew; + int szNew = p->nChar; + szNew += N + 1; + if (szNew > p->mxAlloc) + { + mysqlStrAccumReset(p); + p->tooBig = true; + return false; + } + else + { + p->nAlloc = szNew; + } + zNew = (char*)malloc(p->nAlloc); + if (zNew) + { + memcpy(zNew, p->zText, p->nChar); + mysqlStrAccumReset(p); + p->zText = zNew; + } + else + { + p->mallocFailed = true; + mysqlStrAccumReset(p); + return false; + } + } + + bool isLike = false; + std::string testString(z, N); + if (testString.find("LIKE") != std::string::npos || testString.find("like") != std::string::npos) + { + CLog::Log(LOGDEBUG, + "This query part contains a like, we will double backslash in the next field: {}", + testString); + isLike = true; + } + + memcpy(&p->zText[p->nChar], z, N); + p->nChar += N; + return isLike; +} + +/* +** Finish off a string by making sure it is zero-terminated. +** Return a pointer to the resulting string. Return a NULL +** pointer if any kind of error was encountered. +*/ +char* MysqlDatabase::mysqlStrAccumFinish(StrAccum* p) +{ + if (p->zText) + { + p->zText[p->nChar] = 0; + if (p->zText == p->zBase) + { + p->zText = (char*)malloc(p->nChar + 1); + if (p->zText) + { + memcpy(p->zText, p->zBase, p->nChar + 1); + } + else + { + p->mallocFailed = true; + } + } + } + return p->zText; +} + +/* +** Reset an StrAccum string. Reclaim all malloced memory. +*/ +void MysqlDatabase::mysqlStrAccumReset(StrAccum* p) +{ + if (p->zText != p->zBase) + { + free(p->zText); + } + p->zText = 0; +} + +/* +** Initialize a string accumulator +*/ +void MysqlDatabase::mysqlStrAccumInit(StrAccum* p, char* zBase, int n, int mx) +{ + p->zText = p->zBase = zBase; + p->nChar = 0; + p->nAlloc = n; + p->mxAlloc = mx; + p->tooBig = false; + p->mallocFailed = false; +} + +/* +** Print into memory obtained from mysql_malloc(). Omit the internal +** %-conversion extensions. +*/ +std::string MysqlDatabase::mysql_vmprintf(const char* zFormat, va_list ap) +{ + char zBase[MYSQL_PRINT_BUF_SIZE]; + StrAccum acc; + + mysqlStrAccumInit(&acc, zBase, sizeof(zBase), MYSQL_MAX_LENGTH); + mysqlVXPrintf(&acc, 0, zFormat, ap); + return mysqlStrAccumFinish(&acc); +} + +//************* MysqlDataset implementation *************** + +MysqlDataset::MysqlDataset() : Dataset() +{ + haveError = false; + db = NULL; + autorefresh = false; +} + +MysqlDataset::MysqlDataset(MysqlDatabase* newDb) : Dataset(newDb) +{ + haveError = false; + db = newDb; + autorefresh = false; +} + +MysqlDataset::~MysqlDataset() +{ +} + +void MysqlDataset::set_autorefresh(bool val) +{ + autorefresh = val; +} + +//--------- protected functions implementation -----------------// + +MYSQL* MysqlDataset::handle() +{ + if (db != NULL) + { + return static_cast<MysqlDatabase*>(db)->getHandle(); + } + + return NULL; +} + +void MysqlDataset::make_query(StringList& _sql) +{ + std::string query; + if (db == NULL) + throw DbErrors("No Database Connection"); + try + { + if (autocommit) + db->start_transaction(); + + for (const std::string& i : _sql) + { + query = i; + Dataset::parse_sql(query); + if ((static_cast<MysqlDatabase*>(db)->query_with_reconnect(query.c_str())) != MYSQL_OK) + { + throw DbErrors(db->getErrorMsg()); + } + } // end of for + + if (db->in_transaction() && autocommit) + db->commit_transaction(); + + active = true; + ds_state = dsSelect; + if (autorefresh) + refresh(); + } // end of try + catch (...) + { + if (db->in_transaction()) + db->rollback_transaction(); + throw; + } +} + +void MysqlDataset::make_insert() +{ + make_query(insert_sql); + last(); +} + +void MysqlDataset::make_edit() +{ + make_query(update_sql); +} + +void MysqlDataset::make_deletion() +{ + make_query(delete_sql); +} + +void MysqlDataset::fill_fields() +{ + if ((db == NULL) || (result.record_header.empty()) || + (result.records.size() < (unsigned int)frecno)) + return; + + if (fields_object->size() == 0) // Filling columns name + { + const unsigned int ncols = result.record_header.size(); + fields_object->resize(ncols); + for (unsigned int i = 0; i < ncols; i++) + { + (*fields_object)[i].props = result.record_header[i]; + std::string name = result.record_header[i].name; + name2indexMap.insert({str_toLower(name.data()), i}); + } + } + + //Filling result + if (result.records.size() != 0) + { + const sql_record* row = result.records[frecno]; + if (row) + { + const unsigned int ncols = row->size(); + fields_object->resize(ncols); + for (unsigned int i = 0; i < ncols; i++) + (*fields_object)[i].val = row->at(i); + return; + } + } + const unsigned int ncols = result.record_header.size(); + fields_object->resize(ncols); + for (unsigned int i = 0; i < ncols; i++) + (*fields_object)[i].val = ""; +} + +//------------- public functions implementation -----------------// +bool MysqlDataset::dropIndex(const char* table, const char* index) +{ + std::string sql; + std::string sql_prepared; + + sql = "SELECT * FROM information_schema.statistics WHERE TABLE_SCHEMA=DATABASE() AND " + "table_name='%s' AND index_name='%s'"; + sql_prepared = static_cast<MysqlDatabase*>(db)->prepare(sql.c_str(), table, index); + + if (!query(sql_prepared)) + return false; + + if (num_rows()) + { + sql = "ALTER TABLE %s DROP INDEX %s"; + sql_prepared = static_cast<MysqlDatabase*>(db)->prepare(sql.c_str(), table, index); + + if (exec(sql_prepared) != MYSQL_OK) + return false; + } + + return true; +} + +static bool ci_test(char l, char r) +{ + return tolower(l) == tolower(r); +} + +static size_t ci_find(const std::string& where, const std::string& what) +{ + std::string::const_iterator loc = + std::search(where.begin(), where.end(), what.begin(), what.end(), ci_test); + if (loc == where.end()) + return std::string::npos; + else + return loc - where.begin(); +} + +int MysqlDataset::exec(const std::string& sql) +{ + if (!handle()) + throw DbErrors("No Database Connection"); + std::string qry = sql; + int res = 0; + exec_res.clear(); + + // enforce the "auto_increment" keyword to be appended to "integer primary key" + size_t loc; + + if ((loc = ci_find(qry, "integer primary key")) != std::string::npos) + { + qry = qry.insert(loc + 19, " auto_increment "); + } + + // force the charset and collation to UTF-8 + if (ci_find(qry, "CREATE TABLE") != std::string::npos || + ci_find(qry, "CREATE TEMPORARY TABLE") != std::string::npos) + { + // If CREATE TABLE ... SELECT Syntax is used we need to add the encoding after the table before the select + // e.g. CREATE TABLE x CHARACTER SET utf8 COLLATE utf8_general_ci [AS] SELECT * FROM y + if ((loc = qry.find(" AS SELECT ")) != std::string::npos || + (loc = qry.find(" SELECT ")) != std::string::npos) + { + qry = qry.insert(loc, " CHARACTER SET utf8 COLLATE utf8_general_ci"); + } + else + qry += " CHARACTER SET utf8 COLLATE utf8_general_ci"; + } + + CLog::Log(LOGDEBUG, "Mysql execute: {}", qry); + + if (db->setErr(static_cast<MysqlDatabase*>(db)->query_with_reconnect(qry.c_str()), qry.c_str()) != + MYSQL_OK) + { + throw DbErrors(db->getErrorMsg()); + } + else + { + //! @todo collect results and store in exec_res + return res; + } +} + +int MysqlDataset::exec() +{ + return exec(sql); +} + +const void* MysqlDataset::getExecRes() +{ + return &exec_res; +} + +bool MysqlDataset::query(const std::string& query) +{ + if (!handle()) + throw DbErrors("No Database Connection"); + std::string qry = query; + int fs = qry.find("select"); + int fS = qry.find("SELECT"); + if (!(fs >= 0 || fS >= 0)) + throw DbErrors("MUST be select SQL!"); + + close(); + + size_t loc; + + // mysql doesn't understand CAST(foo as integer) => change to CAST(foo as signed integer) + while ((loc = ci_find(qry, "as integer)")) != std::string::npos) + qry = qry.insert(loc + 3, "signed "); + + MYSQL_RES* stmt = NULL; + + if (static_cast<MysqlDatabase*>(db)->setErr( + static_cast<MysqlDatabase*>(db)->query_with_reconnect(qry.c_str()), qry.c_str()) != + MYSQL_OK) + throw DbErrors(db->getErrorMsg()); + + MYSQL* conn = handle(); + stmt = mysql_store_result(conn); + if (stmt == NULL) + throw DbErrors("Missing result set!"); + + // column headers + const unsigned int numColumns = mysql_num_fields(stmt); + MYSQL_FIELD* fields = mysql_fetch_fields(stmt); + MYSQL_ROW row; + result.record_header.resize(numColumns); + for (unsigned int i = 0; i < numColumns; i++) + result.record_header[i].name = fields[i].name; + + // returned rows + while ((row = mysql_fetch_row(stmt))) + { // have a row of data + sql_record* res = new sql_record; + res->resize(numColumns); + for (unsigned int i = 0; i < numColumns; i++) + { + field_value& v = res->at(i); + switch (fields[i].type) + { + case MYSQL_TYPE_LONGLONG: + if (row[i] != nullptr) + { + v.set_asInt64(strtoll(row[i], nullptr, 10)); + } + else + { + v.set_asInt64(0); + } + break; + case MYSQL_TYPE_DECIMAL: + case MYSQL_TYPE_NEWDECIMAL: + case MYSQL_TYPE_TINY: + case MYSQL_TYPE_SHORT: + case MYSQL_TYPE_INT24: + case MYSQL_TYPE_LONG: + if (row[i] != NULL) + { + v.set_asInt(atoi(row[i])); + } + else + { + v.set_asInt(0); + } + break; + case MYSQL_TYPE_FLOAT: + case MYSQL_TYPE_DOUBLE: + if (row[i] != NULL) + { + v.set_asDouble(atof(row[i])); + } + else + { + v.set_asDouble(0); + } + break; + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_VARCHAR: + if (row[i] != NULL) + v.set_asString((const char*)row[i]); + break; + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + case MYSQL_TYPE_BLOB: + if (row[i] != NULL) + v.set_asString((const char*)row[i]); + break; + case MYSQL_TYPE_NULL: + default: + CLog::Log(LOGDEBUG, "MYSQL: Unknown field type: {}", fields[i].type); + v.set_asString(""); + v.set_isNull(); + break; + } + } + result.records.push_back(res); + } + mysql_free_result(stmt); + active = true; + ds_state = dsSelect; + this->first(); + return true; +} + +void MysqlDataset::open(const std::string& sql) +{ + set_select_sql(sql); + open(); +} + +void MysqlDataset::open() +{ + if (select_sql.size()) + { + query(select_sql); + } + else + { + ds_state = dsInactive; + } +} + +void MysqlDataset::close() +{ + Dataset::close(); + result.clear(); + edit_object->clear(); + fields_object->clear(); + ds_state = dsInactive; + active = false; +} + +void MysqlDataset::cancel() +{ + if ((ds_state == dsInsert) || (ds_state == dsEdit)) + { + if (result.record_header.size()) + ds_state = dsSelect; + else + ds_state = dsInactive; + } +} + +int MysqlDataset::num_rows() +{ + return result.records.size(); +} + +bool MysqlDataset::eof() +{ + return feof; +} + +bool MysqlDataset::bof() +{ + return fbof; +} + +void MysqlDataset::first() +{ + Dataset::first(); + this->fill_fields(); +} + +void MysqlDataset::last() +{ + Dataset::last(); + fill_fields(); +} + +void MysqlDataset::prev(void) +{ + Dataset::prev(); + fill_fields(); +} + +void MysqlDataset::next(void) +{ + Dataset::next(); + if (!eof()) + fill_fields(); +} + +void MysqlDataset::free_row(void) +{ + if (frecno < 0 || (unsigned int)frecno >= result.records.size()) + return; + + sql_record* row = result.records[frecno]; + if (row) + { + delete row; + result.records[frecno] = NULL; + } +} + +bool MysqlDataset::seek(int pos) +{ + if (ds_state == dsSelect) + { + Dataset::seek(pos); + fill_fields(); + return true; + } + + return false; +} + +int64_t MysqlDataset::lastinsertid() +{ + if (!handle()) + throw DbErrors("No Database Connection"); + return mysql_insert_id(handle()); +} + +long MysqlDataset::nextid(const char* seq_name) +{ + if (handle()) + return db->nextid(seq_name); + + return DB_UNEXPECTED_RESULT; +} + +void MysqlDataset::interrupt() +{ + // Impossible +} + +} // namespace dbiplus |