db.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. #include "db.h"
  2. #include "utils.h"
  3. #include <SQLiteCpp/VariadicBind.h>
  4. #include <chrono>
  5. #include <iomanip>
  6. #include <iostream>
  7. #include <sstream>
  8. #include <thread>
  9. /*
  10. database is locked
  11. This happens when more then one node plays the game.
  12. The database access is slow.
  13. So, make sure you set it up so that you do your writes right
  14. before you collect user input. That way, the user won't see
  15. the lags.
  16. This might be an issue on rPI systems!
  17. Change the strategy so we only update when the game ends.
  18. */
  19. DBData::DBData(void)
  20. : db("space-data.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE) {
  21. locked_retries = 5;
  22. create_tables();
  23. stmt_getSet = std::make_unique<SQLite::Statement>(
  24. db, "SELECT value FROM settings WHERE username=? AND setting=?");
  25. stmt_setSet = std::make_unique<SQLite::Statement>(
  26. db, "REPLACE INTO settings(username, setting, value) VALUES(?,?,?);");
  27. }
  28. DBData::~DBData() {}
  29. #define DBLOCK "database is locked"
  30. /**
  31. * @brief create tables if they don't exist.
  32. */
  33. void DBData::create_tables(void) {
  34. int tries = locked_retries;
  35. retry:
  36. try {
  37. db.exec("CREATE TABLE IF NOT EXISTS \
  38. settings(username TEXT, setting TEXT, value TEXT, \
  39. PRIMARY KEY(username, setting));");
  40. db.exec("CREATE TABLE IF NOT EXISTS \
  41. scores ( \"username\" TEXT, \"when\" INTEGER, \
  42. \"date\" INTEGER, \"hand\" INTEGER, \"won\" INTEGER, \"score\" INTEGER, \
  43. PRIMARY KEY(\"username\", \"date\", \"hand\"));");
  44. } catch (std::exception &e) {
  45. if (get_logger) {
  46. get_logger() << "create_tables():" << std::endl;
  47. get_logger() << "SQLite exception: " << e.what() << std::endl;
  48. }
  49. if (strcmp(e.what(), DBLOCK) == 0) {
  50. --tries;
  51. if (tries > 0) {
  52. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  53. goto retry;
  54. }
  55. if (get_logger)
  56. get_logger() << "giving up! " << locked_retries << " retries."
  57. << std::endl;
  58. }
  59. }
  60. }
  61. /**
  62. * @brief get setting from the settings table
  63. *
  64. * We use user and setting.
  65. * Return isMissing if not found.
  66. *
  67. * @param setting
  68. * @param ifMissing
  69. * @return std::string
  70. */
  71. std::string DBData::getSetting(const std::string &setting,
  72. std::string ifMissing) {
  73. int tries = locked_retries;
  74. retry:
  75. try {
  76. stmt_getSet->reset();
  77. stmt_getSet->bind(1, user);
  78. stmt_getSet->bind(2, setting);
  79. if (stmt_getSet->executeStep()) {
  80. std::string value = stmt_getSet->getColumn(0);
  81. return value;
  82. };
  83. return ifMissing;
  84. } catch (std::exception &e) {
  85. if (get_logger) {
  86. get_logger() << "getSettings( " << setting << "," << ifMissing
  87. << " ): " << user << std::endl;
  88. get_logger() << "SQLite exception: " << e.what() << std::endl;
  89. }
  90. if (strcmp(e.what(), DBLOCK) == 0) {
  91. --tries;
  92. if (tries > 0) {
  93. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  94. goto retry;
  95. }
  96. if (get_logger)
  97. get_logger() << "giving up! " << locked_retries << " retries."
  98. << std::endl;
  99. }
  100. }
  101. return ifMissing;
  102. }
  103. /**
  104. * @brief save setting in the settings table
  105. *
  106. * We save user setting in the settings table.
  107. * We use SQLite's REPLACE INTO so it does an update if it exists, or an insert
  108. * if it is missing.
  109. * @param setting
  110. * @param value
  111. */
  112. void DBData::setSetting(const std::string &setting, const std::string &value) {
  113. int tries = locked_retries;
  114. retry:
  115. try {
  116. stmt_setSet->reset();
  117. stmt_setSet->bind(1, user);
  118. stmt_setSet->bind(2, setting);
  119. stmt_setSet->bind(3, value);
  120. stmt_setSet->exec();
  121. } catch (std::exception &e) {
  122. if (get_logger) {
  123. get_logger() << "setSettings( " << setting << "," << value
  124. << " ): " << user << std::endl;
  125. get_logger() << "SQLite exception: " << e.what() << std::endl;
  126. }
  127. if (strcmp(e.what(), DBLOCK) == 0) {
  128. --tries;
  129. if (tries > 0) {
  130. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  131. goto retry;
  132. }
  133. if (get_logger)
  134. get_logger() << "giving up! " << locked_retries << " retries."
  135. << std::endl;
  136. }
  137. }
  138. }
  139. /**
  140. * @brief save the user's score
  141. *
  142. * @param when now()
  143. * @param date what day they played
  144. * @param hand which hand they played
  145. * @param won did they win? 1/0
  146. * @param score
  147. */
  148. void DBData::saveScore(time_t when, time_t date, int hand, int won, int score) {
  149. int tries = locked_retries;
  150. retry:
  151. try {
  152. SQLite::Statement stmt(
  153. db, "INSERT INTO scores( \"username\", \"when\", "
  154. "\"date\", \"hand\", \"won\", \"score\") VALUES(?,?,?,?,?,?);");
  155. stmt.bind(1, user);
  156. stmt.bind(2, when);
  157. stmt.bind(3, date);
  158. stmt.bind(4, hand);
  159. stmt.bind(5, won);
  160. stmt.bind(6, score);
  161. stmt.exec();
  162. } catch (std::exception &e) {
  163. if (get_logger) {
  164. get_logger() << "saveScore( " << when << "," << date << "," << hand << ","
  165. << won << "," << score << " ): " << user << std::endl;
  166. get_logger() << "SQLite exception: " << e.what() << std::endl;
  167. }
  168. if (strcmp(e.what(), DBLOCK) == 0) {
  169. --tries;
  170. if (tries > 0) {
  171. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  172. goto retry;
  173. }
  174. if (get_logger)
  175. get_logger() << "giving up! " << locked_retries << " retries."
  176. << std::endl;
  177. }
  178. }
  179. }
  180. /**
  181. * @brief Returns number of hands played on given day
  182. *
  183. * returns number of hands they played, or 0
  184. * @param day
  185. * @return int
  186. */
  187. int DBData::handsPlayedOnDay(time_t day) {
  188. int tries = locked_retries;
  189. retry:
  190. try {
  191. SQLite::Statement stmt(
  192. db, "SELECT COUNT(*) FROM scores WHERE \"username\"=? AND \"DATE\"=?;");
  193. stmt.bind(1, user);
  194. stmt.bind(2, day);
  195. int count = 0;
  196. if (stmt.executeStep()) {
  197. count = stmt.getColumn(0);
  198. };
  199. return count;
  200. } catch (std::exception &e) {
  201. if (get_logger) {
  202. get_logger() << "handsPlayedOnDay( " << day << " ): " << user
  203. << std::endl;
  204. get_logger() << "SQLite exception: " << e.what() << std::endl;
  205. }
  206. if (strcmp(e.what(), DBLOCK) == 0) {
  207. --tries;
  208. if (tries > 0) {
  209. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  210. goto retry;
  211. }
  212. if (get_logger)
  213. get_logger() << "giving up! " << locked_retries << " retries."
  214. << std::endl;
  215. }
  216. }
  217. return 0;
  218. }
  219. /*
  220. * If you're looking at scores, you're not really looking for all the details.
  221. * I think using the group/SUM would be better, and it sorts scores from highest
  222. * to lowest. Let SQL do the work for me.
  223. */
  224. // select date,username,SUM(score),SUM(won) FROM scores group by date,username
  225. // ORDER BY SUM(score) DESC;
  226. std::vector<scores_details> DBData::getScoresOnDay(time_t date) {
  227. std::vector<scores_details> scores;
  228. int tries = locked_retries;
  229. retry:
  230. try {
  231. // \"when\",
  232. SQLite::Statement stmt(db, "SELECT \"username\", \"date\", \"hand\", "
  233. "\"won\", \"score\" FROM SCORES WHERE "
  234. "\"date\"=? ORDER BY \"username\", \"hand\";");
  235. stmt.bind(1, date);
  236. while (stmt.executeStep()) {
  237. scores_details sd;
  238. sd.user = (const char *)stmt.getColumn(0);
  239. sd.date = stmt.getColumn(1);
  240. sd.hand = stmt.getColumn(2);
  241. sd.won = stmt.getColumn(3);
  242. sd.score = stmt.getColumn(4);
  243. scores.push_back(sd);
  244. }
  245. } catch (std::exception &e) {
  246. if (get_logger) {
  247. get_logger() << "getScoresOnDay( " << date << " ): " << std::endl;
  248. get_logger() << "SQLite exception: " << e.what() << std::endl;
  249. }
  250. scores.clear();
  251. if (strcmp(e.what(), DBLOCK) == 0) {
  252. --tries;
  253. if (tries > 0) {
  254. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  255. goto retry;
  256. }
  257. if (get_logger)
  258. get_logger() << "giving up! " << locked_retries << " retries."
  259. << std::endl;
  260. }
  261. }
  262. return scores;
  263. }
  264. /**
  265. * @brief Gets scores, time_t is day, vector has user and scores sorted highest
  266. * to lowest.
  267. *
  268. * @return std::map<time_t, std::vector<scores_data>>
  269. */
  270. std::map<time_t, std::vector<scores_data>> DBData::getScores(void) {
  271. std::map<time_t, std::vector<scores_data>> scores;
  272. int tries = locked_retries;
  273. retry:
  274. try {
  275. SQLite::Statement stmt(
  276. db, "SELECT \"date\",\"username\",SUM(score),SUM(won) FROM scores "
  277. "GROUP BY \"date\",username ORDER BY \"date\",SUM(score) DESC;");
  278. time_t current = 0;
  279. std::vector<scores_data> vsd;
  280. while (stmt.executeStep()) {
  281. time_t the_date = stmt.getColumn(0);
  282. if (current == 0) {
  283. // ok, we've got the first one!
  284. current = the_date;
  285. } else {
  286. // Ok, are we on another date now?
  287. if (the_date != current) {
  288. scores[current] = std::move(vsd);
  289. vsd.clear();
  290. current = the_date;
  291. }
  292. }
  293. scores_data sd;
  294. sd.user = (const char *)stmt.getColumn(1);
  295. sd.date = the_date;
  296. sd.score = stmt.getColumn(2);
  297. sd.won = stmt.getColumn(3);
  298. vsd.push_back(sd);
  299. }
  300. if (!vsd.empty()) {
  301. scores[current] = std::move(vsd);
  302. }
  303. vsd.clear();
  304. } catch (std::exception &e) {
  305. if (get_logger) {
  306. get_logger() << "getScores(): " << std::endl;
  307. get_logger() << "SQLite exception: " << e.what() << std::endl;
  308. }
  309. scores.clear();
  310. if (strcmp(e.what(), DBLOCK) == 0) {
  311. --tries;
  312. if (tries > 0) {
  313. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  314. goto retry;
  315. }
  316. if (get_logger)
  317. get_logger() << "giving up! " << locked_retries << " retries."
  318. << std::endl;
  319. }
  320. }
  321. return scores;
  322. }
  323. /**
  324. * @brief Get hands played per day
  325. *
  326. * Uses the user value.
  327. *
  328. * @return std::map<time_t, int>
  329. */
  330. std::map<time_t, int> DBData::getPlayed(void) {
  331. std::map<time_t, int> hands;
  332. int tries = locked_retries;
  333. retry:
  334. try {
  335. SQLite::Statement stmt(
  336. // select date, count(hand) from scores where username='grinder' group
  337. // by date;
  338. db, "SELECT \"date\",COUNT(hand) FROM scores "
  339. "WHERE username=? GROUP BY \"date\";");
  340. stmt.bind(1, user);
  341. while (stmt.executeStep()) {
  342. time_t the_date = stmt.getColumn(0);
  343. hands[the_date] = stmt.getColumn(1);
  344. }
  345. } catch (std::exception &e) {
  346. if (get_logger) {
  347. get_logger() << "getPlayed(): " << user << std::endl;
  348. get_logger() << "SQLite exception: " << e.what() << std::endl;
  349. }
  350. hands.clear();
  351. if (strcmp(e.what(), DBLOCK) == 0) {
  352. --tries;
  353. if (tries > 0) {
  354. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  355. goto retry;
  356. }
  357. if (get_logger)
  358. get_logger() << "giving up! " << locked_retries << " retries."
  359. << std::endl;
  360. }
  361. }
  362. return hands;
  363. }
  364. /**
  365. * @brief When has the user played?
  366. *
  367. * This returns a map of date (time_t), and number of hands played on that date.
  368. *
  369. * @return std::map<time_t, int>
  370. */
  371. std::map<time_t, int> DBData::whenPlayed(void) {
  372. // select "date", count(hand) from scores where username='?' group by
  373. // "date";
  374. std::map<time_t, int> plays;
  375. int tries = locked_retries;
  376. retry:
  377. try {
  378. SQLite::Statement stmt(db, "SELECT \"date\", COUNT(hand) FROM scores WHERE "
  379. "username='?' GROUP BY \"date\";");
  380. stmt.bind(1, user);
  381. while (stmt.executeStep()) {
  382. plays[stmt.getColumn(0)] = stmt.getColumn(1);
  383. }
  384. } catch (std::exception &e) {
  385. if (get_logger) {
  386. get_logger() << "whenPlayed(): " << std::endl;
  387. get_logger() << "SQLite exception: " << e.what() << std::endl;
  388. }
  389. plays.clear();
  390. if (strcmp(e.what(), DBLOCK) == 0) {
  391. --tries;
  392. if (tries > 0) {
  393. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  394. goto retry;
  395. }
  396. if (get_logger)
  397. get_logger() << "giving up! " << locked_retries << " retries."
  398. << std::endl;
  399. }
  400. }
  401. return plays;
  402. }
  403. /**
  404. * @brief This will expire out old scores
  405. *
  406. * @todo implement, but don't throw away high scores.
  407. *
  408. */
  409. void DBData::expireScores(void) {}
  410. /**
  411. * @brief Format date to string.
  412. *
  413. * We use default "%0m/%0d/%Y", but can be configured by SysOp via
  414. * config["date_score"] setting. "%Y/%0m/%0d" for non-US?
  415. *
  416. * @param tt
  417. * @return std::string
  418. */
  419. std::string convertDateToDateScoreFormat(time_t tt) {
  420. std::stringstream ss;
  421. if (config["date_score"]) {
  422. std::string custom_format = config["date_score"].as<std::string>();
  423. ss << std::put_time(std::localtime(&tt), custom_format.c_str());
  424. } else {
  425. ss << std::put_time(std::localtime(&tt), "%0m/%0d/%Y");
  426. }
  427. std::string date = ss.str();
  428. return date;
  429. }
  430. void normalizeDate(std::chrono::_V2::system_clock::time_point &date) {
  431. time_t date_t = std::chrono::system_clock::to_time_t(date);
  432. normalizeDate(date_t);
  433. date = std::chrono::system_clock::from_time_t(date_t);
  434. }
  435. /**
  436. * @brief change datetime to have consistent time
  437. *
  438. * This converts the time part to hour:00
  439. *
  440. * @param tt
  441. * @param hour
  442. */
  443. void normalizeDate(time_t &tt, int hour) {
  444. std::tm *local_tm = localtime(&tt);
  445. // adjust date to 2:00:00 AM
  446. tt -= (local_tm->tm_min * 60) + local_tm->tm_sec;
  447. while (local_tm->tm_hour < hour) {
  448. ++local_tm->tm_hour;
  449. tt += 60 * 60;
  450. }
  451. if (local_tm->tm_hour > hour) {
  452. tt -= (60 * 60) * (local_tm->tm_hour - hour);
  453. }
  454. /* // possible DST adjustment. LMTATSM
  455. if (local_tm->tm_isdst) {
  456. // DST in effect
  457. tt -= (60*60);
  458. }
  459. */
  460. }