|
|
@@ -0,0 +1,372 @@
|
|
|
+#include "mysql_client.hpp"
|
|
|
+#include "logging/logger.hpp"
|
|
|
+#if __has_include(<mariadb/mysql.h>)
|
|
|
+#include <mariadb/mysql.h>
|
|
|
+#else
|
|
|
+#include <mysql/mysql.h>
|
|
|
+#endif
|
|
|
+
|
|
|
+namespace smartbotic::runner::mysql {
|
|
|
+
|
|
|
+using namespace common;
|
|
|
+
|
|
|
+MysqlClient::MysqlClient(const MysqlCredentials& credentials)
|
|
|
+ : credentials_(credentials) {
|
|
|
+}
|
|
|
+
|
|
|
+MysqlClient::~MysqlClient() {
|
|
|
+ disconnect();
|
|
|
+}
|
|
|
+
|
|
|
+Result<void> MysqlClient::connect() {
|
|
|
+ if (connected_) {
|
|
|
+ return Result<void>();
|
|
|
+ }
|
|
|
+
|
|
|
+ MYSQL* mysql = mysql_init(nullptr);
|
|
|
+ if (!mysql) {
|
|
|
+ return Error(ErrorCode::Internal, "Failed to initialize MySQL client");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set connection options
|
|
|
+ unsigned int timeout = 10;
|
|
|
+ mysql_options(mysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout);
|
|
|
+ mysql_options(mysql, MYSQL_OPT_READ_TIMEOUT, &timeout);
|
|
|
+ mysql_options(mysql, MYSQL_OPT_WRITE_TIMEOUT, &timeout);
|
|
|
+
|
|
|
+ if (credentials_.use_ssl) {
|
|
|
+ mysql_ssl_set(mysql, nullptr, nullptr, nullptr, nullptr, nullptr);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Connect
|
|
|
+ MYSQL* result = mysql_real_connect(
|
|
|
+ mysql,
|
|
|
+ credentials_.host.c_str(),
|
|
|
+ credentials_.username.c_str(),
|
|
|
+ credentials_.password.c_str(),
|
|
|
+ credentials_.database.empty() ? nullptr : credentials_.database.c_str(),
|
|
|
+ credentials_.port,
|
|
|
+ nullptr, // Unix socket
|
|
|
+ 0 // Client flags
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!result) {
|
|
|
+ std::string error = mysql_error(mysql);
|
|
|
+ mysql_close(mysql);
|
|
|
+ return Error(ErrorCode::Unavailable, "MySQL connection failed: " + error);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set character set to UTF-8
|
|
|
+ mysql_set_character_set(mysql, "utf8mb4");
|
|
|
+
|
|
|
+ connection_ = mysql;
|
|
|
+ connected_ = true;
|
|
|
+
|
|
|
+ LOG_DEBUG("MySQL connected to {}:{}/{}", credentials_.host, credentials_.port, credentials_.database);
|
|
|
+
|
|
|
+ return Result<void>();
|
|
|
+}
|
|
|
+
|
|
|
+void MysqlClient::disconnect() {
|
|
|
+ if (connection_) {
|
|
|
+ mysql_close(static_cast<MYSQL*>(connection_));
|
|
|
+ connection_ = nullptr;
|
|
|
+ connected_ = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool MysqlClient::isConnected() const {
|
|
|
+ if (!connected_ || !connection_) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // Ping to check connection
|
|
|
+ return mysql_ping(static_cast<MYSQL*>(connection_)) == 0;
|
|
|
+}
|
|
|
+
|
|
|
+std::string MysqlClient::escape(const std::string& str) {
|
|
|
+ if (!connection_) {
|
|
|
+ return str;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string escaped;
|
|
|
+ escaped.resize(str.size() * 2 + 1);
|
|
|
+
|
|
|
+ unsigned long len = mysql_real_escape_string(
|
|
|
+ static_cast<MYSQL*>(connection_),
|
|
|
+ escaped.data(),
|
|
|
+ str.c_str(),
|
|
|
+ str.size()
|
|
|
+ );
|
|
|
+
|
|
|
+ escaped.resize(len);
|
|
|
+ return escaped;
|
|
|
+}
|
|
|
+
|
|
|
+std::string MysqlClient::formatValue(const nlohmann::json& value) {
|
|
|
+ if (value.is_null()) {
|
|
|
+ return "NULL";
|
|
|
+ }
|
|
|
+ if (value.is_boolean()) {
|
|
|
+ return value.get<bool>() ? "1" : "0";
|
|
|
+ }
|
|
|
+ if (value.is_number_integer()) {
|
|
|
+ return std::to_string(value.get<int64_t>());
|
|
|
+ }
|
|
|
+ if (value.is_number_float()) {
|
|
|
+ return std::to_string(value.get<double>());
|
|
|
+ }
|
|
|
+ if (value.is_string()) {
|
|
|
+ return "'" + escape(value.get<std::string>()) + "'";
|
|
|
+ }
|
|
|
+ // For arrays/objects, convert to JSON string
|
|
|
+ return "'" + escape(value.dump()) + "'";
|
|
|
+}
|
|
|
+
|
|
|
+QueryResult MysqlClient::query(const std::string& sql, const std::vector<nlohmann::json>& params) {
|
|
|
+ QueryResult result;
|
|
|
+
|
|
|
+ // Auto-connect if not connected
|
|
|
+ if (!connected_) {
|
|
|
+ auto conn_result = connect();
|
|
|
+ if (conn_result.failed()) {
|
|
|
+ result.error = conn_result.error().message();
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ MYSQL* mysql = static_cast<MYSQL*>(connection_);
|
|
|
+
|
|
|
+ // Replace ? placeholders with actual values
|
|
|
+ std::string final_sql = sql;
|
|
|
+ size_t param_idx = 0;
|
|
|
+ size_t pos = 0;
|
|
|
+
|
|
|
+ while ((pos = final_sql.find('?', pos)) != std::string::npos) {
|
|
|
+ if (param_idx >= params.size()) {
|
|
|
+ result.error = "Not enough parameters for query placeholders";
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string value_str = formatValue(params[param_idx]);
|
|
|
+ final_sql.replace(pos, 1, value_str);
|
|
|
+ pos += value_str.size();
|
|
|
+ param_idx++;
|
|
|
+ }
|
|
|
+
|
|
|
+ LOG_DEBUG("MySQL query: {}", final_sql.substr(0, 200));
|
|
|
+
|
|
|
+ // Execute query
|
|
|
+ if (mysql_real_query(mysql, final_sql.c_str(), final_sql.size()) != 0) {
|
|
|
+ result.error = mysql_error(mysql);
|
|
|
+ LOG_ERROR("MySQL query failed: {}", result.error);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get result
|
|
|
+ MYSQL_RES* res = mysql_store_result(mysql);
|
|
|
+
|
|
|
+ if (res) {
|
|
|
+ // SELECT query with results
|
|
|
+ unsigned int num_fields = mysql_num_fields(res);
|
|
|
+ MYSQL_FIELD* fields = mysql_fetch_fields(res);
|
|
|
+
|
|
|
+ // Get column names
|
|
|
+ for (unsigned int i = 0; i < num_fields; i++) {
|
|
|
+ result.columns.push_back(fields[i].name);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fetch rows
|
|
|
+ MYSQL_ROW row;
|
|
|
+ unsigned long* lengths;
|
|
|
+
|
|
|
+ while ((row = mysql_fetch_row(res))) {
|
|
|
+ lengths = mysql_fetch_lengths(res);
|
|
|
+ nlohmann::json row_obj = nlohmann::json::object();
|
|
|
+
|
|
|
+ for (unsigned int i = 0; i < num_fields; i++) {
|
|
|
+ if (row[i] == nullptr) {
|
|
|
+ row_obj[fields[i].name] = nullptr;
|
|
|
+ } else {
|
|
|
+ // Determine value type based on field type
|
|
|
+ std::string value(row[i], lengths[i]);
|
|
|
+
|
|
|
+ switch (fields[i].type) {
|
|
|
+ case MYSQL_TYPE_TINY:
|
|
|
+ case MYSQL_TYPE_SHORT:
|
|
|
+ case MYSQL_TYPE_LONG:
|
|
|
+ case MYSQL_TYPE_LONGLONG:
|
|
|
+ case MYSQL_TYPE_INT24:
|
|
|
+ case MYSQL_TYPE_YEAR:
|
|
|
+ try {
|
|
|
+ row_obj[fields[i].name] = std::stoll(value);
|
|
|
+ } catch (...) {
|
|
|
+ row_obj[fields[i].name] = value;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case MYSQL_TYPE_FLOAT:
|
|
|
+ case MYSQL_TYPE_DOUBLE:
|
|
|
+ case MYSQL_TYPE_DECIMAL:
|
|
|
+ case MYSQL_TYPE_NEWDECIMAL:
|
|
|
+ try {
|
|
|
+ row_obj[fields[i].name] = std::stod(value);
|
|
|
+ } catch (...) {
|
|
|
+ row_obj[fields[i].name] = value;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ row_obj[fields[i].name] = value;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ result.rows.push_back(row_obj);
|
|
|
+ }
|
|
|
+
|
|
|
+ mysql_free_result(res);
|
|
|
+ result.affected_rows = static_cast<int64_t>(result.rows.size());
|
|
|
+ } else {
|
|
|
+ // Non-SELECT query or no results
|
|
|
+ if (mysql_field_count(mysql) == 0) {
|
|
|
+ // INSERT, UPDATE, DELETE, etc.
|
|
|
+ result.affected_rows = static_cast<int64_t>(mysql_affected_rows(mysql));
|
|
|
+ result.insert_id = static_cast<int64_t>(mysql_insert_id(mysql));
|
|
|
+ } else {
|
|
|
+ // Query should have returned results but didn't
|
|
|
+ result.error = mysql_error(mysql);
|
|
|
+ if (!result.error.empty()) {
|
|
|
+ LOG_ERROR("MySQL result error: {}", result.error);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ result.success = true;
|
|
|
+ LOG_DEBUG("MySQL query completed: {} rows, {} affected", result.rows.size(), result.affected_rows);
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+Result<std::vector<std::string>> MysqlClient::listDatabases() {
|
|
|
+ auto conn_result = connect();
|
|
|
+ if (conn_result.failed()) {
|
|
|
+ return conn_result.error();
|
|
|
+ }
|
|
|
+
|
|
|
+ auto query_result = query("SHOW DATABASES");
|
|
|
+ if (!query_result.success) {
|
|
|
+ return Error(ErrorCode::Internal, query_result.error);
|
|
|
+ }
|
|
|
+
|
|
|
+ std::vector<std::string> databases;
|
|
|
+ for (const auto& row : query_result.rows) {
|
|
|
+ if (row.contains("Database")) {
|
|
|
+ databases.push_back(row["Database"].get<std::string>());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return databases;
|
|
|
+}
|
|
|
+
|
|
|
+Result<std::vector<TableInfo>> MysqlClient::listTables(const std::string& database) {
|
|
|
+ auto conn_result = connect();
|
|
|
+ if (conn_result.failed()) {
|
|
|
+ return conn_result.error();
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string db = database.empty() ? credentials_.database : database;
|
|
|
+ if (db.empty()) {
|
|
|
+ return Error(ErrorCode::InvalidArgument, "No database specified");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use the database
|
|
|
+ auto use_result = useDatabase(db);
|
|
|
+ if (use_result.failed()) {
|
|
|
+ return use_result.error();
|
|
|
+ }
|
|
|
+
|
|
|
+ auto query_result = query("SHOW FULL TABLES");
|
|
|
+ if (!query_result.success) {
|
|
|
+ return Error(ErrorCode::Internal, query_result.error);
|
|
|
+ }
|
|
|
+
|
|
|
+ std::vector<TableInfo> tables;
|
|
|
+ for (const auto& row : query_result.rows) {
|
|
|
+ TableInfo info;
|
|
|
+
|
|
|
+ // Column names are dynamic based on database name
|
|
|
+ for (auto& [key, value] : row.items()) {
|
|
|
+ if (key.find("Tables_in_") == 0) {
|
|
|
+ info.name = value.get<std::string>();
|
|
|
+ } else if (key == "Table_type") {
|
|
|
+ info.type = value.get<std::string>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!info.name.empty()) {
|
|
|
+ tables.push_back(info);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return tables;
|
|
|
+}
|
|
|
+
|
|
|
+Result<std::vector<ColumnInfo>> MysqlClient::listColumns(const std::string& table, const std::string& database) {
|
|
|
+ auto conn_result = connect();
|
|
|
+ if (conn_result.failed()) {
|
|
|
+ return conn_result.error();
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string db = database.empty() ? credentials_.database : database;
|
|
|
+ if (db.empty()) {
|
|
|
+ return Error(ErrorCode::InvalidArgument, "No database specified");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use the database
|
|
|
+ auto use_result = useDatabase(db);
|
|
|
+ if (use_result.failed()) {
|
|
|
+ return use_result.error();
|
|
|
+ }
|
|
|
+
|
|
|
+ auto query_result = query("DESCRIBE `" + escape(table) + "`");
|
|
|
+ if (!query_result.success) {
|
|
|
+ return Error(ErrorCode::Internal, query_result.error);
|
|
|
+ }
|
|
|
+
|
|
|
+ std::vector<ColumnInfo> columns;
|
|
|
+ for (const auto& row : query_result.rows) {
|
|
|
+ ColumnInfo info;
|
|
|
+ info.name = row.value("Field", "");
|
|
|
+ info.type = row.value("Type", "");
|
|
|
+ info.nullable = row.value("Null", "NO") == "YES";
|
|
|
+ info.key = row.value("Key", "");
|
|
|
+ info.default_value = row.contains("Default") && !row["Default"].is_null()
|
|
|
+ ? row["Default"].get<std::string>() : "";
|
|
|
+ info.extra = row.value("Extra", "");
|
|
|
+
|
|
|
+ columns.push_back(info);
|
|
|
+ }
|
|
|
+
|
|
|
+ return columns;
|
|
|
+}
|
|
|
+
|
|
|
+Result<void> MysqlClient::useDatabase(const std::string& database) {
|
|
|
+ if (!connected_ || !connection_) {
|
|
|
+ auto conn_result = connect();
|
|
|
+ if (conn_result.failed()) {
|
|
|
+ return conn_result.error();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ MYSQL* mysql = static_cast<MYSQL*>(connection_);
|
|
|
+
|
|
|
+ if (mysql_select_db(mysql, database.c_str()) != 0) {
|
|
|
+ return Error(ErrorCode::NotFound, "Database not found: " + database);
|
|
|
+ }
|
|
|
+
|
|
|
+ return Result<void>();
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace smartbotic::runner::mysql
|