1#include "connection.h"
2
3#include <engine/server/databases/connection_pool.h>
4
5#if defined(CONF_MYSQL)
6#include <base/dbg.h>
7#include <base/mem.h>
8#include <base/sphore.h>
9#include <base/str.h>
10
11#include <engine/console.h>
12
13#include <mysql.h>
14
15#include <atomic>
16#include <memory>
17#include <vector>
18
19// MySQL >= 8.0.1 removed my_bool, 8.0.2 accidentally reintroduced it: https://bugs.mysql.com/bug.php?id=87337
20#if !defined(LIBMARIADB) && MYSQL_VERSION_ID >= 80001 && MYSQL_VERSION_ID != 80002
21typedef bool my_bool;
22#endif
23
24enum
25{
26 MYSQLSTATE_UNINITIALIZED,
27 MYSQLSTATE_INITIALIZED,
28 MYSQLSTATE_SHUTTINGDOWN,
29};
30
31std::atomic_int g_MysqlState = {MYSQLSTATE_UNINITIALIZED};
32std::atomic_int g_MysqlNumConnections;
33
34bool MysqlAvailable()
35{
36 return true;
37}
38
39int MysqlInit()
40{
41 dbg_assert(mysql_thread_safe(), "MySQL library without thread safety");
42 dbg_assert(g_MysqlState == MYSQLSTATE_UNINITIALIZED, "double MySQL initialization");
43 if(mysql_library_init(argc: 0, argv: nullptr, groups: nullptr))
44 {
45 return 1;
46 }
47 int Uninitialized = MYSQLSTATE_UNINITIALIZED;
48 bool Swapped = g_MysqlState.compare_exchange_strong(i1&: Uninitialized, i2: MYSQLSTATE_INITIALIZED);
49 (void)Swapped;
50 dbg_assert(Swapped, "MySQL double initialization");
51 return 0;
52}
53
54void MysqlUninit()
55{
56 int Initialized = MYSQLSTATE_INITIALIZED;
57 bool Swapped = g_MysqlState.compare_exchange_strong(i1&: Initialized, i2: MYSQLSTATE_SHUTTINGDOWN);
58 (void)Swapped;
59 dbg_assert(Swapped, "double MySQL free or free without initialization");
60 int Counter = g_MysqlNumConnections;
61 if(Counter != 0)
62 {
63 dbg_msg(sys: "mysql", fmt: "can't deinitialize, connections remaining: %d", Counter);
64 return;
65 }
66 mysql_library_end();
67}
68
69class CMysqlConnection : public IDbConnection
70{
71public:
72 explicit CMysqlConnection(CMysqlConfig m_Config);
73 ~CMysqlConnection();
74 void Print(IConsole *pConsole, const char *pMode) override;
75
76 const char *BinaryCollate() const override { return "utf8mb4_bin"; }
77 void ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize) override;
78 const char *InsertTimestampAsUtc() const override { return "?"; }
79 const char *CollateNocase() const override { return "CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci"; }
80 const char *InsertIgnore() const override { return "INSERT IGNORE"; }
81 const char *Random() const override { return "RAND()"; }
82 const char *MedianMapTime(char *pBuffer, int BufferSize) const override;
83 const char *False() const override { return "FALSE"; }
84 const char *True() const override { return "TRUE"; }
85
86 bool Connect(char *pError, int ErrorSize) override;
87 void Disconnect() override;
88
89 bool PrepareStatement(const char *pStmt, char *pError, int ErrorSize) override;
90
91 void BindString(int Idx, const char *pString) override;
92 void BindBlob(int Idx, unsigned char *pBlob, int Size) override;
93 void BindInt(int Idx, int Value) override;
94 void BindInt64(int Idx, int64_t Value) override;
95 void BindFloat(int Idx, float Value) override;
96 void BindNull(int Idx) override;
97
98 void Print() override {}
99 bool Step(bool *pEnd, char *pError, int ErrorSize) override;
100 bool ExecuteUpdate(int *pNumUpdated, char *pError, int ErrorSize) override;
101
102 bool IsNull(int Col) override;
103 float GetFloat(int Col) override;
104 int GetInt(int Col) override;
105 int64_t GetInt64(int Col) override;
106 void GetString(int Col, char *pBuffer, int BufferSize) override;
107 int GetBlob(int Col, unsigned char *pBuffer, int BufferSize) override;
108
109 bool AddPoints(const char *pPlayer, int Points, char *pError, int ErrorSize) override;
110
111private:
112 class CStmtDeleter
113 {
114 public:
115 void operator()(MYSQL_STMT *pStmt) const;
116 };
117
118 char m_aErrorDetail[128];
119 void StoreErrorMysql(const char *pContext);
120 void StoreErrorStmt(const char *pContext);
121 bool ConnectImpl();
122 bool PrepareAndExecuteStatement(const char *pStmt);
123
124 union UParameterExtra
125 {
126 int i;
127 int64_t i64;
128 unsigned long ul;
129 float f;
130 };
131
132 bool m_NewQuery = false;
133 bool m_HaveConnection = false;
134 MYSQL m_Mysql;
135 std::unique_ptr<MYSQL_STMT, CStmtDeleter> m_pStmt = nullptr;
136 std::vector<MYSQL_BIND> m_vStmtParameters;
137 std::vector<UParameterExtra> m_vStmtParameterExtras;
138
139 // copy of m_Config vars
140 CMysqlConfig m_Config;
141
142 std::atomic_bool m_InUse;
143};
144
145void CMysqlConnection::CStmtDeleter::operator()(MYSQL_STMT *pStmt) const
146{
147 mysql_stmt_close(stmt: pStmt);
148}
149
150CMysqlConnection::CMysqlConnection(CMysqlConfig Config) :
151 IDbConnection(Config.m_aPrefix),
152 m_Config(Config),
153 m_InUse(false)
154{
155 g_MysqlNumConnections += 1;
156 dbg_assert(g_MysqlState == MYSQLSTATE_INITIALIZED, "MySQL library not in initialized state");
157
158 m_aErrorDetail[0] = '\0';
159 mem_zero(block: &m_Mysql, size: sizeof(m_Mysql));
160 mysql_init(mysql: &m_Mysql);
161}
162
163CMysqlConnection::~CMysqlConnection()
164{
165 mysql_close(sock: &m_Mysql);
166 g_MysqlNumConnections -= 1;
167}
168
169void CMysqlConnection::StoreErrorMysql(const char *pContext)
170{
171 str_format(buffer: m_aErrorDetail, buffer_size: sizeof(m_aErrorDetail), format: "(%s:mysql:%d): %s", pContext, mysql_errno(mysql: &m_Mysql), mysql_error(mysql: &m_Mysql));
172}
173
174void CMysqlConnection::StoreErrorStmt(const char *pContext)
175{
176 str_format(buffer: m_aErrorDetail, buffer_size: sizeof(m_aErrorDetail), format: "(%s:stmt:%d): %s", pContext, mysql_stmt_errno(stmt: m_pStmt.get()), mysql_stmt_error(stmt: m_pStmt.get()));
177}
178
179bool CMysqlConnection::PrepareAndExecuteStatement(const char *pStmt)
180{
181 if(mysql_stmt_prepare(stmt: m_pStmt.get(), query: pStmt, length: str_length(str: pStmt)))
182 {
183 StoreErrorStmt(pContext: "prepare");
184 return false;
185 }
186 if(mysql_stmt_execute(stmt: m_pStmt.get()))
187 {
188 StoreErrorStmt(pContext: "execute");
189 return false;
190 }
191 return true;
192}
193
194void CMysqlConnection::Print(IConsole *pConsole, const char *pMode)
195{
196 char aBuf[512];
197 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
198 format: "MySQL-%s: DB: '%s' Prefix: '%s' User: '%s' IP: <{'%s'}> Port: %d",
199 pMode, m_Config.m_aDatabase, GetPrefix(), m_Config.m_aUser, m_Config.m_aIp, m_Config.m_Port);
200 pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
201}
202
203void CMysqlConnection::ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize)
204{
205 str_format(buffer: aBuf, buffer_size: BufferSize, format: "UNIX_TIMESTAMP(%s)", pTimestamp);
206}
207
208bool CMysqlConnection::Connect(char *pError, int ErrorSize)
209{
210 dbg_assert(!m_InUse.exchange(true), "Tried connecting while the connection is in use");
211
212 m_NewQuery = true;
213 if(!ConnectImpl())
214 {
215 str_copy(dst: pError, src: m_aErrorDetail, dst_size: ErrorSize);
216 m_InUse.store(i: false);
217 return false;
218 }
219 return true;
220}
221
222bool CMysqlConnection::ConnectImpl()
223{
224 if(m_HaveConnection)
225 {
226 if(m_pStmt && mysql_stmt_free_result(stmt: m_pStmt.get()))
227 {
228 StoreErrorStmt(pContext: "free_result");
229 dbg_msg(sys: "mysql", fmt: "can't free last result %s", m_aErrorDetail);
230 }
231 if(!mysql_select_db(mysql: &m_Mysql, db: m_Config.m_aDatabase))
232 {
233 // Success.
234 return true;
235 }
236 StoreErrorMysql(pContext: "select_db");
237 dbg_msg(sys: "mysql", fmt: "ping error, trying to reconnect %s", m_aErrorDetail);
238 mysql_close(sock: &m_Mysql);
239 mem_zero(block: &m_Mysql, size: sizeof(m_Mysql));
240 mysql_init(mysql: &m_Mysql);
241 }
242
243 m_pStmt = nullptr;
244 unsigned int OptConnectTimeout = 60;
245 unsigned int OptReadTimeout = 60;
246 unsigned int OptWriteTimeout = 120;
247 my_bool OptReconnect = true;
248 mysql_options(mysql: &m_Mysql, option: MYSQL_OPT_CONNECT_TIMEOUT, arg: &OptConnectTimeout);
249 mysql_options(mysql: &m_Mysql, option: MYSQL_OPT_READ_TIMEOUT, arg: &OptReadTimeout);
250 mysql_options(mysql: &m_Mysql, option: MYSQL_OPT_WRITE_TIMEOUT, arg: &OptWriteTimeout);
251 mysql_options(mysql: &m_Mysql, option: MYSQL_OPT_RECONNECT, arg: &OptReconnect);
252 mysql_options(mysql: &m_Mysql, option: MYSQL_SET_CHARSET_NAME, arg: "utf8mb4");
253 if(m_Config.m_aBindaddr[0] != '\0')
254 {
255 mysql_options(mysql: &m_Mysql, option: MYSQL_OPT_BIND, arg: m_Config.m_aBindaddr);
256 }
257
258 if(!mysql_real_connect(mysql: &m_Mysql, host: m_Config.m_aIp, user: m_Config.m_aUser, passwd: m_Config.m_aPass, db: nullptr, port: m_Config.m_Port, unix_socket: nullptr, CLIENT_IGNORE_SIGPIPE))
259 {
260 StoreErrorMysql(pContext: "real_connect");
261 return false;
262 }
263 m_HaveConnection = true;
264
265 m_pStmt = std::unique_ptr<MYSQL_STMT, CStmtDeleter>(mysql_stmt_init(mysql: &m_Mysql));
266
267 // Apparently MYSQL_SET_CHARSET_NAME is not enough
268 if(!PrepareAndExecuteStatement(pStmt: "SET CHARACTER SET utf8mb4"))
269 {
270 return false;
271 }
272
273 if(m_Config.m_Setup)
274 {
275 char aCreateDatabase[1024];
276 // create database
277 str_format(buffer: aCreateDatabase, buffer_size: sizeof(aCreateDatabase), format: "CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4", m_Config.m_aDatabase);
278 if(!PrepareAndExecuteStatement(pStmt: aCreateDatabase))
279 {
280 return false;
281 }
282 }
283
284 // Connect to specific database
285 if(mysql_select_db(mysql: &m_Mysql, db: m_Config.m_aDatabase))
286 {
287 StoreErrorMysql(pContext: "select_db");
288 return false;
289 }
290
291 if(m_Config.m_Setup)
292 {
293 char aCreateRace[1024];
294 char aCreateTeamrace[1024];
295 char aCreateMaps[1024];
296 char aCreateSaves[1024];
297 char aCreatePoints[1024];
298 FormatCreateRace(aBuf: aCreateRace, BufferSize: sizeof(aCreateRace), /* Backup */ false);
299 FormatCreateTeamrace(aBuf: aCreateTeamrace, BufferSize: sizeof(aCreateTeamrace), pIdType: "VARBINARY(16)", /* Backup */ false);
300 FormatCreateMaps(aBuf: aCreateMaps, BufferSize: sizeof(aCreateMaps));
301 FormatCreateSaves(aBuf: aCreateSaves, BufferSize: sizeof(aCreateSaves), /* Backup */ false);
302 FormatCreatePoints(aBuf: aCreatePoints, BufferSize: sizeof(aCreatePoints));
303
304 if(!PrepareAndExecuteStatement(pStmt: aCreateRace) ||
305 !PrepareAndExecuteStatement(pStmt: aCreateTeamrace) ||
306 !PrepareAndExecuteStatement(pStmt: aCreateMaps) ||
307 !PrepareAndExecuteStatement(pStmt: aCreateSaves) ||
308 !PrepareAndExecuteStatement(pStmt: aCreatePoints))
309 {
310 return false;
311 }
312 m_Config.m_Setup = false;
313 }
314 dbg_msg(sys: "mysql", fmt: "connection established");
315 return true;
316}
317
318void CMysqlConnection::Disconnect()
319{
320 m_InUse.store(i: false);
321}
322
323bool CMysqlConnection::PrepareStatement(const char *pStmt, char *pError, int ErrorSize)
324{
325 if(mysql_stmt_prepare(stmt: m_pStmt.get(), query: pStmt, length: str_length(str: pStmt)))
326 {
327 StoreErrorStmt(pContext: "prepare");
328 str_copy(dst: pError, src: m_aErrorDetail, dst_size: ErrorSize);
329 return false;
330 }
331 m_NewQuery = true;
332 unsigned NumParameters = mysql_stmt_param_count(stmt: m_pStmt.get());
333 m_vStmtParameters.resize(new_size: NumParameters);
334 m_vStmtParameterExtras.resize(new_size: NumParameters);
335 if(NumParameters)
336 {
337 mem_zero(block: m_vStmtParameters.data(), size: sizeof(m_vStmtParameters[0]) * m_vStmtParameters.size());
338 mem_zero(block: m_vStmtParameterExtras.data(), size: sizeof(m_vStmtParameterExtras[0]) * m_vStmtParameterExtras.size());
339 }
340 return true;
341}
342
343void CMysqlConnection::BindString(int Idx, const char *pString)
344{
345 m_NewQuery = true;
346 Idx -= 1;
347 dbg_assert(0 <= Idx && Idx < (int)m_vStmtParameters.size(), "Error in BindString: index out of bounds: %d", Idx);
348
349 int Length = str_length(str: pString);
350 m_vStmtParameterExtras[Idx].ul = Length;
351 MYSQL_BIND *pParam = &m_vStmtParameters[Idx];
352 pParam->buffer_type = MYSQL_TYPE_STRING;
353 pParam->buffer = (void *)pString;
354 pParam->buffer_length = Length + 1;
355 pParam->length = &m_vStmtParameterExtras[Idx].ul;
356 pParam->is_null = nullptr;
357 pParam->is_unsigned = false;
358 pParam->error = nullptr;
359}
360
361void CMysqlConnection::BindBlob(int Idx, unsigned char *pBlob, int Size)
362{
363 m_NewQuery = true;
364 Idx -= 1;
365 dbg_assert(0 <= Idx && Idx < (int)m_vStmtParameters.size(), "Error in BindBlob: index out of bounds: %d", Idx);
366
367 m_vStmtParameterExtras[Idx].ul = Size;
368 MYSQL_BIND *pParam = &m_vStmtParameters[Idx];
369 pParam->buffer_type = MYSQL_TYPE_BLOB;
370 pParam->buffer = pBlob;
371 pParam->buffer_length = Size;
372 pParam->length = &m_vStmtParameterExtras[Idx].ul;
373 pParam->is_null = nullptr;
374 pParam->is_unsigned = false;
375 pParam->error = nullptr;
376}
377
378void CMysqlConnection::BindInt(int Idx, int Value)
379{
380 m_NewQuery = true;
381 Idx -= 1;
382 dbg_assert(0 <= Idx && Idx < (int)m_vStmtParameters.size(), "Error in BindInt: index out of bounds: %d", Idx);
383
384 m_vStmtParameterExtras[Idx].i = Value;
385 MYSQL_BIND *pParam = &m_vStmtParameters[Idx];
386 pParam->buffer_type = MYSQL_TYPE_LONG;
387 pParam->buffer = &m_vStmtParameterExtras[Idx].i;
388 pParam->buffer_length = sizeof(m_vStmtParameterExtras[Idx].i);
389 pParam->length = nullptr;
390 pParam->is_null = nullptr;
391 pParam->is_unsigned = false;
392 pParam->error = nullptr;
393}
394
395void CMysqlConnection::BindInt64(int Idx, int64_t Value)
396{
397 m_NewQuery = true;
398 Idx -= 1;
399 dbg_assert(0 <= Idx && Idx < (int)m_vStmtParameters.size(), "Error in BindInt64: index out of bounds: %d", Idx);
400
401 m_vStmtParameterExtras[Idx].i64 = Value;
402 MYSQL_BIND *pParam = &m_vStmtParameters[Idx];
403 pParam->buffer_type = MYSQL_TYPE_LONGLONG;
404 pParam->buffer = &m_vStmtParameterExtras[Idx].i64;
405 pParam->buffer_length = sizeof(m_vStmtParameterExtras[Idx].i64);
406 pParam->length = nullptr;
407 pParam->is_null = nullptr;
408 pParam->is_unsigned = false;
409 pParam->error = nullptr;
410}
411
412void CMysqlConnection::BindFloat(int Idx, float Value)
413{
414 m_NewQuery = true;
415 Idx -= 1;
416 dbg_assert(0 <= Idx && Idx < (int)m_vStmtParameters.size(), "Error in BindFloat: index out of bounds: %d", Idx);
417
418 m_vStmtParameterExtras[Idx].f = Value;
419 MYSQL_BIND *pParam = &m_vStmtParameters[Idx];
420 pParam->buffer_type = MYSQL_TYPE_FLOAT;
421 pParam->buffer = &m_vStmtParameterExtras[Idx].f;
422 pParam->buffer_length = sizeof(m_vStmtParameterExtras[Idx].i);
423 pParam->length = nullptr;
424 pParam->is_null = nullptr;
425 pParam->is_unsigned = false;
426 pParam->error = nullptr;
427}
428
429void CMysqlConnection::BindNull(int Idx)
430{
431 m_NewQuery = true;
432 Idx -= 1;
433 dbg_assert(0 <= Idx && Idx < (int)m_vStmtParameters.size(), "Error in BindNull: index out of bounds: %d", Idx);
434
435 MYSQL_BIND *pParam = &m_vStmtParameters[Idx];
436 pParam->buffer_type = MYSQL_TYPE_NULL;
437 pParam->buffer = nullptr;
438 pParam->buffer_length = 0;
439 pParam->length = nullptr;
440 pParam->is_null = nullptr;
441 pParam->is_unsigned = false;
442 pParam->error = nullptr;
443}
444
445bool CMysqlConnection::Step(bool *pEnd, char *pError, int ErrorSize)
446{
447 if(m_NewQuery)
448 {
449 m_NewQuery = false;
450 if(mysql_stmt_bind_param(stmt: m_pStmt.get(), bnd: m_vStmtParameters.data()))
451 {
452 StoreErrorStmt(pContext: "bind_param");
453 str_copy(dst: pError, src: m_aErrorDetail, dst_size: ErrorSize);
454 return false;
455 }
456 if(mysql_stmt_execute(stmt: m_pStmt.get()))
457 {
458 StoreErrorStmt(pContext: "execute");
459 str_copy(dst: pError, src: m_aErrorDetail, dst_size: ErrorSize);
460 return false;
461 }
462 }
463 int Result = mysql_stmt_fetch(stmt: m_pStmt.get());
464 if(Result == 1)
465 {
466 StoreErrorStmt(pContext: "fetch");
467 str_copy(dst: pError, src: m_aErrorDetail, dst_size: ErrorSize);
468 return false;
469 }
470 *pEnd = (Result == MYSQL_NO_DATA);
471 // `Result` is now either `MYSQL_DATA_TRUNCATED` (which we ignore, we
472 // fetch our columns in a different way) or `0` aka success.
473 return true;
474}
475
476bool CMysqlConnection::ExecuteUpdate(int *pNumUpdated, char *pError, int ErrorSize)
477{
478 if(m_NewQuery)
479 {
480 m_NewQuery = false;
481 if(mysql_stmt_bind_param(stmt: m_pStmt.get(), bnd: m_vStmtParameters.data()))
482 {
483 StoreErrorStmt(pContext: "bind_param");
484 str_copy(dst: pError, src: m_aErrorDetail, dst_size: ErrorSize);
485 return false;
486 }
487 if(mysql_stmt_execute(stmt: m_pStmt.get()))
488 {
489 StoreErrorStmt(pContext: "execute");
490 str_copy(dst: pError, src: m_aErrorDetail, dst_size: ErrorSize);
491 return false;
492 }
493 *pNumUpdated = mysql_stmt_affected_rows(stmt: m_pStmt.get());
494 return true;
495 }
496 str_copy(dst: pError, src: "tried to execute update without query", dst_size: ErrorSize);
497 return false;
498}
499
500bool CMysqlConnection::IsNull(int Col)
501{
502 Col -= 1;
503
504 MYSQL_BIND Bind;
505 my_bool IsNull;
506 mem_zero(block: &Bind, size: sizeof(Bind));
507 Bind.buffer_type = MYSQL_TYPE_NULL;
508 Bind.buffer = nullptr;
509 Bind.buffer_length = 0;
510 Bind.length = nullptr;
511 Bind.is_null = &IsNull;
512 Bind.is_unsigned = false;
513 Bind.error = nullptr;
514 if(mysql_stmt_fetch_column(stmt: m_pStmt.get(), bind_arg: &Bind, column: Col, offset: 0))
515 {
516 StoreErrorStmt(pContext: "fetch_column:null");
517 dbg_assert_failed("Error in IsNull(%d): error fetching column %s", Col + 1, m_aErrorDetail);
518 }
519 return IsNull;
520}
521
522float CMysqlConnection::GetFloat(int Col)
523{
524 Col -= 1;
525
526 MYSQL_BIND Bind;
527 float Value;
528 my_bool IsNull;
529 mem_zero(block: &Bind, size: sizeof(Bind));
530 Bind.buffer_type = MYSQL_TYPE_FLOAT;
531 Bind.buffer = &Value;
532 Bind.buffer_length = sizeof(Value);
533 Bind.length = nullptr;
534 Bind.is_null = &IsNull;
535 Bind.is_unsigned = false;
536 Bind.error = nullptr;
537 if(mysql_stmt_fetch_column(stmt: m_pStmt.get(), bind_arg: &Bind, column: Col, offset: 0))
538 {
539 StoreErrorStmt(pContext: "fetch_column:float");
540 dbg_assert_failed("Error in GetFloat(%d): error fetching column %s", Col + 1, m_aErrorDetail);
541 }
542 dbg_assert(!IsNull, "Error in GetFloat(%d): NULL", Col + 1);
543 return Value;
544}
545
546int CMysqlConnection::GetInt(int Col)
547{
548 Col -= 1;
549
550 MYSQL_BIND Bind;
551 int Value;
552 my_bool IsNull;
553 mem_zero(block: &Bind, size: sizeof(Bind));
554 Bind.buffer_type = MYSQL_TYPE_LONG;
555 Bind.buffer = &Value;
556 Bind.buffer_length = sizeof(Value);
557 Bind.length = nullptr;
558 Bind.is_null = &IsNull;
559 Bind.is_unsigned = false;
560 Bind.error = nullptr;
561 if(mysql_stmt_fetch_column(stmt: m_pStmt.get(), bind_arg: &Bind, column: Col, offset: 0))
562 {
563 StoreErrorStmt(pContext: "fetch_column:int");
564 dbg_assert_failed("Error in GetInt(%d): error fetching column %s", Col + 1, m_aErrorDetail);
565 }
566 dbg_assert(!IsNull, "Error in GetInt(%d): NULL", Col + 1);
567 return Value;
568}
569
570int64_t CMysqlConnection::GetInt64(int Col)
571{
572 Col -= 1;
573
574 MYSQL_BIND Bind;
575 int64_t Value;
576 my_bool IsNull;
577 mem_zero(block: &Bind, size: sizeof(Bind));
578 Bind.buffer_type = MYSQL_TYPE_LONGLONG;
579 Bind.buffer = &Value;
580 Bind.buffer_length = sizeof(Value);
581 Bind.length = nullptr;
582 Bind.is_null = &IsNull;
583 Bind.is_unsigned = false;
584 Bind.error = nullptr;
585 if(mysql_stmt_fetch_column(stmt: m_pStmt.get(), bind_arg: &Bind, column: Col, offset: 0))
586 {
587 StoreErrorStmt(pContext: "fetch_column:int64");
588 dbg_assert_failed("Error in GetInt64(%d): error fetching column %s", Col + 1, m_aErrorDetail);
589 }
590 dbg_assert(!IsNull, "Error in GetInt64(%d): NULL", Col + 1);
591 return Value;
592}
593
594void CMysqlConnection::GetString(int Col, char *pBuffer, int BufferSize)
595{
596 Col -= 1;
597
598 for(int i = 0; i < BufferSize; i++)
599 {
600 pBuffer[i] = '\0';
601 }
602
603 MYSQL_BIND Bind;
604 unsigned long Length;
605 my_bool IsNull;
606 my_bool Error;
607 mem_zero(block: &Bind, size: sizeof(Bind));
608 Bind.buffer_type = MYSQL_TYPE_STRING;
609 Bind.buffer = pBuffer;
610 // leave one character for null-termination
611 Bind.buffer_length = BufferSize - 1;
612 Bind.length = &Length;
613 Bind.is_null = &IsNull;
614 Bind.is_unsigned = false;
615 Bind.error = &Error;
616 if(mysql_stmt_fetch_column(stmt: m_pStmt.get(), bind_arg: &Bind, column: Col, offset: 0))
617 {
618 StoreErrorStmt(pContext: "fetch_column:string");
619 dbg_assert_failed("Error in GetString(%d): error fetching column %s", Col + 1, m_aErrorDetail);
620 }
621 dbg_assert(!IsNull, "Error in GetString(%d): NULL", Col + 1);
622 dbg_assert(!Error, "Error in GetString(%d): truncation occurred", Col + 1);
623}
624
625int CMysqlConnection::GetBlob(int Col, unsigned char *pBuffer, int BufferSize)
626{
627 Col -= 1;
628
629 MYSQL_BIND Bind;
630 unsigned long Length;
631 my_bool IsNull;
632 my_bool Error;
633 mem_zero(block: &Bind, size: sizeof(Bind));
634 Bind.buffer_type = MYSQL_TYPE_BLOB;
635 Bind.buffer = pBuffer;
636 Bind.buffer_length = BufferSize;
637 Bind.length = &Length;
638 Bind.is_null = &IsNull;
639 Bind.is_unsigned = false;
640 Bind.error = &Error;
641 if(mysql_stmt_fetch_column(stmt: m_pStmt.get(), bind_arg: &Bind, column: Col, offset: 0))
642 {
643 StoreErrorStmt(pContext: "fetch_column:blob");
644 dbg_assert_failed("Error in GetBlob(%d): error fetching column %s", Col + 1, m_aErrorDetail);
645 }
646 dbg_assert(!IsNull, "Error in GetBlob(%d): NULL", Col + 1);
647 dbg_assert(!Error, "Error in GetBlob(%d): truncation occurred", Col + 1);
648 return Length;
649}
650
651const char *CMysqlConnection::MedianMapTime(char *pBuffer, int BufferSize) const
652{
653 str_format(buffer: pBuffer, buffer_size: BufferSize,
654 format: "SELECT MEDIAN(Time) "
655 "OVER (PARTITION BY Map) "
656 "FROM %s_race "
657 "WHERE Map = l.Map "
658 "LIMIT 1",
659 GetPrefix());
660 return pBuffer;
661}
662
663bool CMysqlConnection::AddPoints(const char *pPlayer, int Points, char *pError, int ErrorSize)
664{
665 char aBuf[512];
666 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
667 format: "INSERT INTO %s_points(Name, Points) "
668 "VALUES (?, ?) "
669 "ON DUPLICATE KEY UPDATE Points=Points+?",
670 GetPrefix());
671 if(!PrepareStatement(pStmt: aBuf, pError, ErrorSize))
672 {
673 return false;
674 }
675 BindString(Idx: 1, pString: pPlayer);
676 BindInt(Idx: 2, Value: Points);
677 BindInt(Idx: 3, Value: Points);
678 int NumUpdated;
679 return ExecuteUpdate(pNumUpdated: &NumUpdated, pError, ErrorSize);
680}
681
682std::unique_ptr<IDbConnection> CreateMysqlConnection(CMysqlConfig Config)
683{
684 return std::make_unique<CMysqlConnection>(args&: Config);
685}
686#else
687bool MysqlAvailable()
688{
689 return false;
690}
691int MysqlInit()
692{
693 return 0;
694}
695void MysqlUninit()
696{
697}
698std::unique_ptr<IDbConnection> CreateMysqlConnection(CMysqlConfig Config)
699{
700 return nullptr;
701}
702#endif
703