cpp
This commit is contained in:
276
jtlsrv-cpp/src/db/pool.cpp
Normal file
276
jtlsrv-cpp/src/db/pool.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user