This commit is contained in:
seb
2026-07-13 07:34:17 +02:00
parent d471f2411d
commit b7b76c9d39
46 changed files with 39025 additions and 7 deletions

276
jtlsrv-cpp/src/db/pool.cpp Normal file
View File

@@ -0,0 +1,276 @@
#include "pool.hpp"
#include "../config.hpp"
#include "../log.hpp"
#include <cstring>
#include <cstdlib>
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<std::mutex> 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<std::mutex> lock(mutex_);
c->in_use = false;
}
bool OdbcPool::execute(const std::string& sql, const std::vector<Param>& 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<std::vector<SQLWCHAR>> wbufs;
// Bind parameters
for (size_t i = 0; i < params.size(); i++) {
const auto& p = params[i];
SQLUSMALLINT param_num = static_cast<SQLUSMALLINT>(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<SQLWCHAR> wbuf(p.str_val.size());
for (size_t k = 0; k < p.str_val.size(); k++)
wbuf[k] = static_cast<SQLWCHAR>((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<SQLSMALLINT> 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<uint8_t> 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<Param>& 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;
}