database.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. /*
  2. * Copyright 2005-2016 Zarafa and its licensors
  3. *
  4. * This program is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License, version 3, as
  6. * published by the Free Software Foundation.
  7. *
  8. * This program is distributed in the hope that it will be useful, but WITHOUT
  9. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  10. * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
  11. * for more details.
  12. *
  13. * You should have received a copy of the GNU Affero General Public License
  14. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. */
  16. #include <memory>
  17. #include <string>
  18. #include <utility>
  19. #include <cassert>
  20. #include <cstring>
  21. #include <mysql.h>
  22. #include <mysqld_error.h>
  23. #include <kopano/ECConfig.h>
  24. #include <kopano/ECLogger.h>
  25. #include <kopano/database.hpp>
  26. #include <kopano/stringutil.h>
  27. #define LOG_SQL_DEBUG(_msg, ...) \
  28. ec_log(EC_LOGLEVEL_DEBUG | EC_LOGLEVEL_SQL, _msg, ##__VA_ARGS__)
  29. DB_RESULT::~DB_RESULT(void)
  30. {
  31. if (m_res == nullptr)
  32. return;
  33. assert(m_db != nullptr);
  34. if (m_db == nullptr)
  35. return;
  36. m_db->FreeResult_internal(m_res);
  37. m_res = nullptr;
  38. }
  39. KDatabase::KDatabase(void)
  40. {
  41. memset(&m_lpMySQL, 0, sizeof(m_lpMySQL));
  42. }
  43. ECRESULT KDatabase::Connect(ECConfig *cfg, bool reconnect,
  44. unsigned int mysql_flags, unsigned int gcm)
  45. {
  46. const char *mysql_port = cfg->GetSetting("mysql_port");
  47. const char *mysql_socket = cfg->GetSetting("mysql_socket");
  48. DB_RESULT result;
  49. DB_ROW row = nullptr;
  50. std::string query;
  51. if (*mysql_socket == '\0')
  52. mysql_socket = nullptr;
  53. auto er = InitEngine(reconnect);
  54. if (er != erSuccess) {
  55. ec_log_crit("KDatabase::Connect(): InitEngine failed %d", er);
  56. goto exit;
  57. }
  58. if (mysql_real_connect(&m_lpMySQL, cfg->GetSetting("mysql_host"),
  59. cfg->GetSetting("mysql_user"), cfg->GetSetting("mysql_password"),
  60. cfg->GetSetting("mysql_database"),
  61. mysql_port ? atoi(mysql_port) : 0,
  62. mysql_socket, mysql_flags) == nullptr) {
  63. if (mysql_errno(&m_lpMySQL) == ER_BAD_DB_ERROR)
  64. /* Database does not exist */
  65. er = KCERR_DATABASE_NOT_FOUND;
  66. else
  67. er = KCERR_DATABASE_ERROR;
  68. ec_log_err("KDatabase::Connect(): database access error %d, mysql error: %s",
  69. er, GetError());
  70. goto exit;
  71. }
  72. // Check if the database is available, but empty
  73. er = DoSelect("SHOW tables", &result);
  74. if (er != erSuccess) {
  75. ec_log_err("KDatabase::Connect(): \"SHOW tables\" failed %d", er);
  76. goto exit;
  77. }
  78. if (GetNumRows(result) == 0) {
  79. er = KCERR_DATABASE_NOT_FOUND;
  80. ec_log_err("KDatabase::Connect(): database missing %d", er);
  81. goto exit;
  82. }
  83. query = "SHOW variables LIKE 'max_allowed_packet'";
  84. er = DoSelect(query, &result);
  85. if (er != erSuccess) {
  86. ec_log_err("KDatabase::Connect(): max_allowed_packet retrieval failed %d", er);
  87. goto exit;
  88. }
  89. row = FetchRow(result);
  90. /* row[0] has the variable name, [1] the value */
  91. if (row == nullptr || row[0] == nullptr || row[1] == nullptr) {
  92. ec_log_warn("Unable to retrieve max_allowed_packet value. Assuming %d.", KC_DFL_MAX_PACKET_SIZE);
  93. m_ulMaxAllowedPacket = KC_DFL_MAX_PACKET_SIZE;
  94. } else {
  95. m_ulMaxAllowedPacket = atoui(row[1]);
  96. }
  97. /*
  98. * Changing the per-session "max_allowed_packet" is not permitted since
  99. * MySQL 5.1, and the global one is for superusers only. Give a warning
  100. * instead, then.
  101. */
  102. if (m_ulMaxAllowedPacket < KC_DFL_MAX_PACKET_SIZE)
  103. ec_log_warn("max_allowed_packet is smaller than 16M (%d). You are advised to increase this value by adding max_allowed_packet=16M in the [mysqld] section of my.cnf.", m_ulMaxAllowedPacket);
  104. m_bConnected = true;
  105. if (mysql_set_character_set(&m_lpMySQL, "utf8")) {
  106. ec_log_err("Unable to set character set to \"utf8\"");
  107. er = KCERR_DATABASE_ERROR;
  108. goto exit;
  109. }
  110. query = "SET SESSION group_concat_max_len = " + stringify(gcm);
  111. if (Query(query) != 0) {
  112. ec_log_crit("KDatabase::Connect(): group_concat_max_len set fail: %s", GetError());
  113. er = KCERR_DATABASE_ERROR;
  114. goto exit;
  115. }
  116. exit:
  117. if (er != erSuccess)
  118. Close();
  119. return er;
  120. }
  121. ECRESULT KDatabase::CreateDatabase(ECConfig *cfg, bool reconnect)
  122. {
  123. const char *dbname = cfg->GetSetting("mysql_database");
  124. const char *mysql_port = cfg->GetSetting("mysql_port");
  125. const char *mysql_socket = cfg->GetSetting("mysql_socket");
  126. if (*mysql_socket == '\0')
  127. mysql_socket = nullptr;
  128. // Kopano archiver database tables
  129. auto er = InitEngine(reconnect);
  130. if (er != erSuccess)
  131. return er;
  132. // Connect
  133. // m_lpMySQL: address of an existing MYSQL, Before calling
  134. // mysql_real_connect(), call mysql_init() to initialize the
  135. // MYSQL structure.
  136. if (mysql_real_connect(&m_lpMySQL, cfg->GetSetting("mysql_host"),
  137. cfg->GetSetting("mysql_user"), cfg->GetSetting("mysql_password"),
  138. nullptr, mysql_port != nullptr ? atoi(mysql_port) : 0,
  139. mysql_socket, 0) == nullptr) {
  140. ec_log_err("Failed to connect to database: %s", GetError());
  141. return KCERR_DATABASE_ERROR;
  142. }
  143. if (dbname == nullptr) {
  144. ec_log_crit("Unable to create database: Unknown database");
  145. return KCERR_DATABASE_ERROR;
  146. }
  147. ec_log_notice("Create database %s", dbname);
  148. er = IsInnoDBSupported();
  149. if (er != erSuccess)
  150. return er;
  151. std::string query;
  152. query = "CREATE DATABASE IF NOT EXISTS `" +
  153. std::string(cfg->GetSetting("mysql_database")) + "`";
  154. if (Query(query) != erSuccess) {
  155. ec_log_err("Unable to create database: %s", GetError());
  156. return KCERR_DATABASE_ERROR;
  157. }
  158. query = "USE `" + std::string(cfg->GetSetting("mysql_database")) + "`";
  159. er = DoInsert(query);
  160. if (er != erSuccess)
  161. return er;
  162. auto tables = GetDatabaseDefs();
  163. for (size_t i = 0; tables[i].lpSQL != nullptr; ++i) {
  164. ec_log_info("Create table: %s", tables[i].lpComment);
  165. er = DoInsert(tables[i].lpSQL);
  166. if (er != erSuccess)
  167. return er;
  168. }
  169. ec_log_info("Database structure has been created");
  170. return erSuccess;
  171. }
  172. ECRESULT KDatabase::Close(void)
  173. {
  174. /* No locking here */
  175. m_bConnected = false;
  176. if (m_bMysqlInitialize)
  177. mysql_close(&m_lpMySQL);
  178. m_bMysqlInitialize = false;
  179. return erSuccess;
  180. }
  181. /**
  182. * Perform a DELETE operation on the database
  183. * @q: (in) INSERT query string
  184. * @aff: (out) (optional) Receives the number of deleted rows
  185. *
  186. * Sends the passed DELETE query to the MySQL server, and optionally the number
  187. * of deleted rows. Returns erSuccess or %KCERR_DATABASE_ERROR.
  188. */
  189. ECRESULT KDatabase::DoDelete(const std::string &q, unsigned int *aff)
  190. {
  191. autolock alk(*this);
  192. return _Update(q, aff);
  193. }
  194. /**
  195. * Perform an INSERT operation on the database
  196. * @q: (in) INSERT query string
  197. * @idp: (out) (optional) Receives the last insert id
  198. * @aff: (out) (optional) Receives the number of inserted rows
  199. *
  200. * Sends the passed INSERT query to the MySQL server, and optionally returns
  201. * the new insert ID and the number of inserted rows.
  202. *
  203. * Returns erSuccess or %KCERR_DATABASE_ERROR.
  204. */
  205. ECRESULT KDatabase::DoInsert(const std::string &q, unsigned int *idp,
  206. unsigned int *aff)
  207. {
  208. autolock alk(*this);
  209. auto er = _Update(q, aff);
  210. if (er == erSuccess && idp != nullptr)
  211. *idp = GetInsertId();
  212. return er;
  213. }
  214. /**
  215. * Perform a SELECT operation on the database
  216. * @q: (in) SELECT query string
  217. * @res_p: (out) Result output
  218. * @stream: (in) Whether data should be streamed instead of stored
  219. *
  220. * Sends the passed SELECT-like (any operation that outputs a result set) query
  221. * to the MySQL server and retrieves the result.
  222. *
  223. * Setting @stream will delay retrieving data from the network until FetchRow()
  224. * is called. The only drawback is that GetRowCount() can therefore not be used
  225. * unless all rows are fetched first. The main reason to use this is to
  226. * conserve memory and increase pipelining (the client can start processing
  227. * data before the server has completed the query)
  228. *
  229. * Returns erSuccess or %KCERR_DATABASE_ERROR.
  230. */
  231. ECRESULT KDatabase::DoSelect(const std::string &q, DB_RESULT *res_p,
  232. bool stream)
  233. {
  234. assert(q.length() != 0);
  235. autolock alk(*this);
  236. if (Query(q) != erSuccess) {
  237. ec_log_err("KDatabsae::DoSelect(): query failed: %s: %s", q.c_str(), GetError());
  238. return KCERR_DATABASE_ERROR;
  239. }
  240. ECRESULT er = erSuccess;
  241. DB_RESULT res;
  242. if (stream)
  243. res = DB_RESULT(this, mysql_use_result(&m_lpMySQL));
  244. else
  245. res = DB_RESULT(this, mysql_store_result(&m_lpMySQL));
  246. if (res == nullptr) {
  247. if (!m_bSuppressLockErrorLogging ||
  248. GetLastError() == DB_E_UNKNOWN)
  249. ec_log_err("SQL [%08lu] result failed: %s, Query: \"%s\"",
  250. m_lpMySQL.thread_id, mysql_error(&m_lpMySQL), q.c_str());
  251. er = KCERR_DATABASE_ERROR;
  252. }
  253. if (res_p != nullptr)
  254. *res_p = std::move(res);
  255. return er;
  256. }
  257. /**
  258. * This function updates a sequence in an atomic fashion - if called correctly;
  259. *
  260. * To make it work correctly, the state of the database connection should *NOT*
  261. * be in a transaction; this would delay committing of the data until a later
  262. * time, causing other concurrent threads to possibly generate the same ID or
  263. * lock while waiting for this transaction to end. So, do not call Begin()
  264. * before calling this function unless you really know what you are doing.
  265. *
  266. * TODO: Measure sequence update calls, currently it is an update.
  267. */
  268. ECRESULT KDatabase::DoSequence(const std::string &seq, unsigned int count,
  269. unsigned long long *firstidp)
  270. {
  271. unsigned int aff = 0;
  272. autolock alk(*this);
  273. /* Attempt to update the sequence in an atomic fashion */
  274. auto er = DoUpdate("UPDATE settings SET value=LAST_INSERT_ID(value+1)+" +
  275. stringify(count - 1) + " WHERE name = '" + seq + "'", &aff);
  276. if (er != erSuccess) {
  277. ec_log_err("KDatabase::DoSequence() UPDATE failed %d", er);
  278. return er;
  279. }
  280. /*
  281. * If the setting was missing, insert it now, starting at sequence 1
  282. * (not 0 for safety - maybe there is some if(ulSequenceId) code
  283. * somewhere).
  284. */
  285. if (aff == 0) {
  286. er = Query("INSERT INTO settings (name, value) VALUES('" +
  287. seq + "',LAST_INSERT_ID(1)+" + stringify(count - 1) + ")");
  288. if (er != erSuccess) {
  289. ec_log_crit("KDatabase::DoSequence() INSERT INTO failed %d", er);
  290. return er;
  291. }
  292. }
  293. *firstidp = mysql_insert_id(&m_lpMySQL);
  294. return er;
  295. }
  296. /**
  297. * Perform an UPDATE operation on the database
  298. * @q: (in) UPDATE query string
  299. * @aff: (out) (optional) Receives the number of affected rows
  300. *
  301. * Sends the passed UPDATE query to the MySQL server, and optionally returns
  302. * the number of affected rows. The affected rows is the number of rows that
  303. * have been MODIFIED, which is not necessarily the number of rows that MATCHED
  304. * the WHERE clause.
  305. *
  306. * Returns erSuccess or %KCERR_DATABASE_ERROR.
  307. */
  308. ECRESULT KDatabase::DoUpdate(const std::string &q, unsigned int *aff)
  309. {
  310. autolock alk(*this);
  311. return _Update(q, aff);
  312. }
  313. std::string KDatabase::Escape(const std::string &s)
  314. {
  315. auto size = s.length() * 2 + 1;
  316. std::unique_ptr<char[]> esc(new char[size]);
  317. memset(esc.get(), 0, size);
  318. mysql_real_escape_string(&m_lpMySQL, esc.get(), s.c_str(), s.length());
  319. return esc.get();
  320. }
  321. std::string KDatabase::EscapeBinary(const unsigned char *data, size_t len)
  322. {
  323. auto size = len * 2 + 1;
  324. std::unique_ptr<char[]> esc(new char[size]);
  325. memset(esc.get(), 0, size);
  326. mysql_real_escape_string(&m_lpMySQL, esc.get(), reinterpret_cast<const char *>(data), len);
  327. return "'" + std::string(esc.get()) + "'";
  328. }
  329. std::string KDatabase::EscapeBinary(const std::string &s)
  330. {
  331. return EscapeBinary(reinterpret_cast<const unsigned char *>(s.c_str()), s.size());
  332. }
  333. DB_ROW KDatabase::FetchRow(DB_RESULT &r)
  334. {
  335. return mysql_fetch_row(static_cast<MYSQL_RES *>(r.get()));
  336. }
  337. DB_LENGTHS KDatabase::FetchRowLengths(DB_RESULT &r)
  338. {
  339. return mysql_fetch_lengths(static_cast<MYSQL_RES *>(r.get()));
  340. }
  341. void KDatabase::FreeResult_internal(void *r)
  342. {
  343. assert(r != nullptr);
  344. if (r != nullptr)
  345. mysql_free_result(static_cast<MYSQL_RES *>(r));
  346. }
  347. unsigned int KDatabase::GetAffectedRows(void)
  348. {
  349. return mysql_affected_rows(&m_lpMySQL);
  350. }
  351. const char *KDatabase::GetError(void)
  352. {
  353. if (!m_bMysqlInitialize)
  354. return "MYSQL not initialized";
  355. return mysql_error(&m_lpMySQL);
  356. }
  357. unsigned int KDatabase::GetInsertId(void)
  358. {
  359. return mysql_insert_id(&m_lpMySQL);
  360. }
  361. DB_ERROR KDatabase::GetLastError(void)
  362. {
  363. switch (mysql_errno(&m_lpMySQL)) {
  364. case ER_LOCK_WAIT_TIMEOUT:
  365. return DB_E_LOCK_WAIT_TIMEOUT;
  366. case ER_LOCK_DEADLOCK:
  367. return DB_E_LOCK_DEADLOCK;
  368. default:
  369. return DB_E_UNKNOWN;
  370. }
  371. }
  372. unsigned int KDatabase::GetNumRows(const DB_RESULT &r) const
  373. {
  374. return mysql_num_rows(static_cast<MYSQL_RES *>(r.get()));
  375. }
  376. ECRESULT KDatabase::InitEngine(bool reconnect)
  377. {
  378. assert(!m_bMysqlInitialize);
  379. if (!m_bMysqlInitialize && mysql_init(&m_lpMySQL) == nullptr) {
  380. ec_log_crit("KDatabase::InitEngine() mysql_init failed");
  381. return KCERR_DATABASE_ERROR;
  382. }
  383. m_bMysqlInitialize = true;
  384. m_lpMySQL.reconnect = reconnect;
  385. return erSuccess;
  386. }
  387. ECRESULT KDatabase::IsInnoDBSupported(void)
  388. {
  389. DB_RESULT res;
  390. DB_ROW row = nullptr;
  391. auto er = DoSelect("SHOW ENGINES", &res);
  392. if (er != erSuccess) {
  393. ec_log_crit("Unable to query supported database engines. Error: %s", GetError());
  394. return er;
  395. }
  396. while ((row = FetchRow(res)) != nullptr) {
  397. if (strcasecmp(row[0], "InnoDB") != 0)
  398. continue;
  399. if (strcasecmp(row[1], "DISABLED") == 0) {
  400. // mysql has run with innodb enabled once, but disabled this.. so check your log.
  401. ec_log_crit("INNODB engine is disabled. Please re-enable the INNODB engine. Check your MySQL log for more information or comment out skip-innodb in the mysql configuration file.");
  402. return KCERR_DATABASE_ERROR;
  403. } else if (strcasecmp(row[1], "YES") != 0 && strcasecmp(row[1], "DEFAULT") != 0) {
  404. // mysql is incorrectly configured or compiled.
  405. ec_log_crit("INNODB engine is not supported. Please enable the INNODB engine in the mysql configuration file.");
  406. return KCERR_DATABASE_ERROR;
  407. }
  408. break;
  409. }
  410. if (row == nullptr) {
  411. ec_log_crit("Unable to find 'InnoDB' engine from the mysql server. Probably INNODB is not supported.");
  412. return KCERR_DATABASE_ERROR;
  413. }
  414. return erSuccess;
  415. }
  416. ECRESULT KDatabase::Query(const std::string &q)
  417. {
  418. LOG_SQL_DEBUG("SQL [%08lu]: \"%s;\"", m_lpMySQL.thread_id, q.c_str());
  419. /* Be binary safe (http://dev.mysql.com/doc/mysql/en/mysql-real-query.html) */
  420. auto err = mysql_real_query(&m_lpMySQL, q.c_str(), q.length());
  421. if (err == 0)
  422. return erSuccess;
  423. /* Callers without reconnect will emit different messages. */
  424. if (m_lpMySQL.reconnect)
  425. ec_log_err("%p: SQL Failed: %s, Query: \"%s\"",
  426. static_cast<void *>(&m_lpMySQL), mysql_error(&m_lpMySQL),
  427. q.c_str());
  428. return KCERR_DATABASE_ERROR;
  429. }
  430. ECRESULT KDatabase::_Update(const std::string &q, unsigned int *aff)
  431. {
  432. if (Query(q) != 0) {
  433. ec_log_err("KDatabase::_Update() query failed: %s: %s",
  434. q.c_str(), GetError());
  435. return KCERR_DATABASE_ERROR;
  436. }
  437. if (aff != nullptr)
  438. *aff = GetAffectedRows();
  439. return erSuccess;
  440. }