#include "pool.hpp" #include "../config.hpp" #include "../log.hpp" #include #include static OdbcPool g_pool; OdbcPool& get_pool() { return g_pool; } OdbcPool::~OdbcPool() { disconnect(); } int OdbcPool::connect() { SQLRETURN rc; rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv_); if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) return -1; SQLSetEnvAttr(henv_, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); std::string server = config::get("MSSQL_SERVER", "localhost"); int port = config::get_int("MSSQL_PORT", 1433); std::string database = config::get("MSSQL_DATABASE", "eazybusiness"); std::string user = config::get("MSSQL_USER"); std::string password = config::get("MSSQL_PASSWORD"); bool encrypt = (config::get("MSSQL_ENCRYPT", "true") != "false"); bool trust_cert = (config::get("MSSQL_TRUST_SERVER_CERTIFICATE", "true") != "false"); if (user.empty()) { logc::warn("MSSQL_USER not set, skipping DB connection"); return -1; } // Connection string std::string conn_str = "DRIVER={ODBC Driver 18 for SQL Server};" "SERVER=" + server + "," + std::to_string(port) + ";" "DATABASE=" + database + ";" "UID=" + user + ";" "PWD=" + password + ";" "Encrypt=" + (encrypt ? std::string("yes") : std::string("Optional")) + ";" "TrustServerCertificate=" + (trust_cert ? std::string("yes") : std::string("no")) + ";"; logc::info("ODBC connecting to %s:%d/%s as %s", server.c_str(), port, database.c_str(), user.c_str()); // Create pool of 4 connections const int POOL_SIZE = 4; conns_.resize(POOL_SIZE); int connected = 0; for (int i = 0; i < POOL_SIZE; i++) { rc = SQLAllocHandle(SQL_HANDLE_DBC, henv_, &conns_[i].hdbc); if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) continue; rc = SQLDriverConnect(conns_[i].hdbc, nullptr, (SQLCHAR*)conn_str.c_str(), SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_COMPLETE); if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { SQLAllocHandle(SQL_HANDLE_STMT, conns_[i].hdbc, &conns_[i].hstmt); connected++; } else { SQLCHAR state[6], msg[SQL_MAX_MESSAGE_LENGTH]; SQLINTEGER native; SQLSMALLINT msg_len; SQLGetDiagRec(SQL_HANDLE_DBC, conns_[i].hdbc, 1, state, &native, msg, sizeof(msg), &msg_len); logc::warn("ODBC connect[%d] failed: %s - %s", i, state, msg); } } if (connected == 0) { logc::error("no ODBC connections established"); return -1; } logc::success("ODBC pool: %d connections to %s/%s", connected, server.c_str(), database.c_str()); return 0; } void OdbcPool::disconnect() { for (auto& c : conns_) { if (c.hstmt != SQL_NULL_HSTMT) SQLFreeHandle(SQL_HANDLE_STMT, c.hstmt); if (c.hdbc != SQL_NULL_HDBC) { SQLDisconnect(c.hdbc); SQLFreeHandle(SQL_HANDLE_DBC, c.hdbc); } } conns_.clear(); if (henv_ != SQL_NULL_HENV) { SQLFreeHandle(SQL_HANDLE_ENV, henv_); henv_ = SQL_NULL_HENV; } } OdbcPool::Connection* OdbcPool::checkout() { std::lock_guard lock(mutex_); for (auto& c : conns_) { if (!c.in_use) { c.in_use = true; return &c; } } return nullptr; } void OdbcPool::release(Connection* c) { std::lock_guard lock(mutex_); c->in_use = false; } bool OdbcPool::execute(const std::string& sql, const std::vector& params, ResultSet& out) { Connection* c = checkout(); if (!c) return false; SQLRETURN rc; // Prepare statement rc = SQLPrepare(c->hstmt, (SQLCHAR*)sql.c_str(), SQL_NTS); if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) { release(c); return false; } // Wide buffer storage for NVarChar parameters (must outlive SQLExecute) std::vector> wbufs; // Bind parameters for (size_t i = 0; i < params.size(); i++) { const auto& p = params[i]; SQLUSMALLINT param_num = static_cast(i + 1); SQLPOINTER val_ptr = nullptr; SQLLEN buf_len = 0; SQLLEN indicator = 0; SQLSMALLINT c_type = SQL_C_CHAR; SQLSMALLINT sql_type = SQL_VARCHAR; switch (p.type) { case ParamType::Int: c_type = SQL_C_SLONG; sql_type = SQL_INTEGER; val_ptr = (SQLPOINTER)&p.int_val; buf_len = sizeof(SQLINTEGER); break; case ParamType::BigInt: c_type = SQL_C_SBIGINT; sql_type = SQL_BIGINT; val_ptr = (SQLPOINTER)&p.int_val; buf_len = sizeof(SQLBIGINT); break; case ParamType::Float: case ParamType::Double: c_type = SQL_C_DOUBLE; sql_type = SQL_DOUBLE; val_ptr = (SQLPOINTER)&p.dbl_val; buf_len = sizeof(SQLDOUBLE); break; case ParamType::NVarChar: { // Convert narrow string to SQLWCHAR (unsigned short = UTF-16) // SQLWCHAR is unsigned short (2 bytes) on this platform std::vector wbuf(p.str_val.size()); for (size_t k = 0; k < p.str_val.size(); k++) wbuf[k] = static_cast((unsigned char)p.str_val[k]); wbufs.push_back(std::move(wbuf)); auto& wb = wbufs.back(); c_type = SQL_C_WCHAR; sql_type = SQL_WVARCHAR; val_ptr = (SQLPOINTER)wb.data(); buf_len = (SQLLEN)(wb.size() * sizeof(SQLWCHAR)); indicator = wb.empty() ? SQL_NULL_DATA : (SQLLEN)(wb.size() * sizeof(SQLWCHAR)); break; } case ParamType::Bit: c_type = SQL_C_BIT; sql_type = SQL_BIT; val_ptr = (SQLPOINTER)&p.int_val; buf_len = 1; break; } rc = SQLBindParameter(c->hstmt, param_num, SQL_PARAM_INPUT, c_type, sql_type, p.str_val.size() + 1, 0, val_ptr, buf_len, &indicator); if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) { release(c); return false; } } // Execute rc = SQLExecute(c->hstmt); if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO && rc != SQL_NO_DATA) { release(c); return false; } // Fetch results if it's a SELECT out.clear(); SQLSMALLINT col_count = 0; SQLNumResultCols(c->hstmt, &col_count); if (col_count > 0) { // Discover column types std::vector col_types(col_count); bool has_blobs = false; for (SQLSMALLINT col = 0; col < col_count; col++) { SQLSMALLINT data_type; SQLDescribeCol(c->hstmt, col + 1, nullptr, 0, nullptr, &data_type, nullptr, nullptr, nullptr); col_types[col] = data_type; if (data_type == SQL_BINARY || data_type == SQL_VARBINARY || data_type == SQL_LONGVARBINARY) has_blobs = true; } while (true) { rc = SQLFetch(c->hstmt); if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) break; Row row; for (SQLSMALLINT col = 0; col < col_count; col++) { Cell cell; SQLLEN ind; SQLSMALLINT sql_type = col_types[col]; // Binary columns: read as binary if (sql_type == SQL_BINARY || sql_type == SQL_VARBINARY || sql_type == SQL_LONGVARBINARY) { std::vector blob_data; unsigned char chunk[8192]; int chunk_count = 0; while (true) { rc = SQLGetData(c->hstmt, col + 1, SQL_C_BINARY, chunk, sizeof(chunk), &ind); if (ind == SQL_NULL_DATA) { break; } if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { SQLLEN copy_len = std::min(ind, (SQLLEN)sizeof(chunk)); if (has_blobs && chunk_count == 0) logc::info(" blob col=%d rc=%d ind=%d copy=%d", col, rc, (int)ind, (int)copy_len); blob_data.insert(blob_data.end(), chunk, chunk + copy_len); chunk_count++; } if (rc == SQL_SUCCESS) break; if (rc != SQL_SUCCESS_WITH_INFO) break; } if (chunk_count > 0) { cell.type = CellType::Blob; cell.blob = std::move(blob_data); if (has_blobs) logc::info(" blob col=%d total=%zu chunks=%d", col, cell.blob.size(), chunk_count); } } else { // String/numeric columns: read as char char buf[4096]; rc = SQLGetData(c->hstmt, col + 1, SQL_C_CHAR, buf, sizeof(buf) - 1, &ind); if (ind == SQL_NULL_DATA) { cell.type = CellType::Null; } else if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { cell.type = CellType::String; cell.str = std::string(buf, std::min((SQLLEN)(sizeof(buf)-1), ind)); } } row.push_back(std::move(cell)); } out.push_back(std::move(row)); } } // Reset statement for reuse SQLFreeStmt(c->hstmt, SQL_UNBIND); SQLFreeStmt(c->hstmt, SQL_CLOSE); release(c); return true; } bool OdbcPool::execute(const std::string& sql, ResultSet& out) { return execute(sql, {}, out); } int64_t OdbcPool::execute_scalar(const std::string& sql, const std::vector& params, int64_t fallback) { ResultSet rs; if (!execute(sql, params, rs) || rs.empty() || rs[0].empty()) return fallback; const auto& cell = rs[0][0]; if (cell.type == CellType::Int64) return cell.i64; if (cell.type == CellType::String) { try { return std::stoll(cell.str); } catch (...) {} } return fallback; }