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