db.cpp 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. #include "db.h"
  2. #include "utils.h"
  3. #include <SQLiteCpp/VariadicBind.h>
  4. #include <iomanip>
  5. #include <iostream>
  6. #include <sstream>
  7. /*
  8. The database access is slow.
  9. So, make sure you set it up so that you do your writes right
  10. before you collect user input. That way, the user won't see
  11. the lags.
  12. This might be an issue on rPI systems!
  13. Change the strategy so we only update when the game ends.
  14. */
  15. DBData::DBData(void)
  16. : db("space-data.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE) {
  17. create_tables();
  18. stmt_getSet = std::make_unique<SQLite::Statement>(
  19. db, "SELECT value FROM settings WHERE username=? AND setting=?");
  20. stmt_setSet = std::make_unique<SQLite::Statement>(
  21. db, "REPLACE INTO settings(username, setting, value) VALUES(?,?,?);");
  22. }
  23. DBData::~DBData() {}
  24. /**
  25. * @brief create tables if they don't exist.
  26. */
  27. void DBData::create_tables(void) {
  28. try {
  29. db.exec("CREATE TABLE IF NOT EXISTS \
  30. settings(username TEXT, setting TEXT, value TEXT, \
  31. PRIMARY KEY(username, setting));");
  32. db.exec("CREATE TABLE IF NOT EXISTS \
  33. scores ( \"username\" TEXT, \"when\" INTEGER, \
  34. \"date\" INTEGER, \"hand\" INTEGER, \"won\" INTEGER, \"score\" INTEGER, \
  35. PRIMARY KEY(\"username\", \"date\", \"hand\"));");
  36. } catch (std::exception &e) {
  37. if (get_logger) {
  38. get_logger() << "create_tables():" << std::endl;
  39. get_logger() << "SQLite exception: " << e.what() << std::endl;
  40. }
  41. }
  42. }
  43. /**
  44. * @brief get setting from the settings table
  45. *
  46. * We use user and setting.
  47. * Return isMissing if not found.
  48. *
  49. * @param setting
  50. * @param ifMissing
  51. * @return std::string
  52. */
  53. std::string DBData::getSetting(const std::string &setting,
  54. std::string ifMissing) {
  55. try {
  56. stmt_getSet->reset();
  57. stmt_getSet->bind(1, user);
  58. stmt_getSet->bind(2, setting);
  59. if (stmt_getSet->executeStep()) {
  60. std::string value = stmt_getSet->getColumn(0);
  61. return value;
  62. };
  63. return ifMissing;
  64. } catch (std::exception &e) {
  65. if (get_logger) {
  66. get_logger() << "getSettings( " << setting << "," << ifMissing
  67. << " ): " << user << std::endl;
  68. get_logger() << "SQLite exception: " << e.what() << std::endl;
  69. }
  70. }
  71. return ifMissing;
  72. }
  73. /**
  74. * @brief save setting in the settings table
  75. *
  76. * We save user setting in the settings table.
  77. * We use SQLite's REPLACE INTO so it does an update if it exists, or an insert
  78. * if it is missing.
  79. * @param setting
  80. * @param value
  81. */
  82. void DBData::setSetting(const std::string &setting, const std::string &value) {
  83. try {
  84. stmt_setSet->reset();
  85. stmt_setSet->bind(1, user);
  86. stmt_setSet->bind(2, setting);
  87. stmt_setSet->bind(3, value);
  88. stmt_setSet->exec();
  89. } catch (std::exception &e) {
  90. if (get_logger) {
  91. get_logger() << "setSettings( " << setting << "," << value
  92. << " ): " << user << std::endl;
  93. get_logger() << "SQLite exception: " << e.what() << std::endl;
  94. }
  95. }
  96. }
  97. /**
  98. * @brief save the user's score
  99. *
  100. * @param when now()
  101. * @param date what day they played
  102. * @param hand which hand they played
  103. * @param won did they win? 1/0
  104. * @param score
  105. */
  106. void DBData::saveScore(time_t when, time_t date, int hand, int won, int score) {
  107. try {
  108. SQLite::Statement stmt(
  109. db, "INSERT INTO scores( \"username\", \"when\", "
  110. "\"date\", \"hand\", \"won\", \"score\") VALUES(?,?,?,?,?,?);");
  111. stmt.bind(1, user);
  112. stmt.bind(2, when);
  113. stmt.bind(3, date);
  114. stmt.bind(4, hand);
  115. stmt.bind(5, won);
  116. stmt.bind(6, score);
  117. stmt.exec();
  118. } catch (std::exception &e) {
  119. if (get_logger) {
  120. get_logger() << "saveScore( " << when << "," << date << "," << hand << ","
  121. << won << "," << score << " ): " << user << std::endl;
  122. get_logger() << "SQLite exception: " << e.what() << std::endl;
  123. }
  124. }
  125. }
  126. /**
  127. * @brief Returns number of hands played on given day
  128. *
  129. * returns number of hands they played, or 0
  130. * @param day
  131. * @return int
  132. */
  133. int DBData::handsPlayedOnDay(time_t day) {
  134. try {
  135. SQLite::Statement stmt(
  136. db, "SELECT COUNT(*) FROM scores WHERE \"username\"=? AND \"DATE\"=?;");
  137. stmt.bind(1, user);
  138. stmt.bind(2, day);
  139. int count = 0;
  140. if (stmt.executeStep()) {
  141. count = stmt.getColumn(0);
  142. };
  143. return count;
  144. } catch (std::exception &e) {
  145. if (get_logger) {
  146. get_logger() << "handsPlayedOnDay( " << day << " ): " << user
  147. << std::endl;
  148. get_logger() << "SQLite exception: " << e.what() << std::endl;
  149. }
  150. }
  151. return 0;
  152. }
  153. /*
  154. * If you're looking at scores, you're not really looking for all the details.
  155. * I think using the group/SUM would be better, and it sorts scores from highest
  156. * to lowest. Let SQL do the work for me.
  157. */
  158. // select date,username,SUM(score),SUM(won) FROM scores group by date,username
  159. // ORDER BY SUM(score) DESC;
  160. std::vector<scores_details> DBData::getScoresOnDay(time_t date) {
  161. std::vector<scores_details> scores;
  162. try {
  163. // \"when\",
  164. SQLite::Statement stmt(db, "SELECT \"username\", \"date\", \"hand\", "
  165. "\"won\", \"score\" FROM SCORES WHERE "
  166. "\"date\"=? ORDER BY \"username\", \"hand\";");
  167. stmt.bind(1, date);
  168. while (stmt.executeStep()) {
  169. scores_details sd;
  170. sd.user = (const char *)stmt.getColumn(0);
  171. sd.date = stmt.getColumn(1);
  172. sd.hand = stmt.getColumn(2);
  173. sd.won = stmt.getColumn(3);
  174. sd.score = stmt.getColumn(4);
  175. scores.push_back(sd);
  176. }
  177. } catch (std::exception &e) {
  178. if (get_logger) {
  179. get_logger() << "getScoresOnDay( " << date << " ): " << std::endl;
  180. get_logger() << "SQLite exception: " << e.what() << std::endl;
  181. }
  182. scores.clear();
  183. }
  184. return scores;
  185. }
  186. std::map<time_t, std::vector<scores_data>> DBData::getScores(void) {
  187. std::map<time_t, std::vector<scores_data>> scores;
  188. try {
  189. SQLite::Statement stmt(
  190. db, "SELECT \"date\",\"username\",SUM(score),SUM(won) FROM scores "
  191. "GROUP BY \"date\",username ORDER BY SUM(score) DESC;");
  192. time_t current = 0;
  193. std::vector<scores_data> vsd;
  194. while (stmt.executeStep()) {
  195. time_t the_date = stmt.getColumn(0);
  196. if (current == 0) {
  197. // ok, we've got the first one!
  198. current = the_date;
  199. } else {
  200. // Ok, are we on another date now?
  201. if (the_date != current) {
  202. scores[current] = std::move(vsd);
  203. vsd.clear();
  204. current = the_date;
  205. }
  206. }
  207. scores_data sd;
  208. sd.user = (const char *)stmt.getColumn(1);
  209. sd.date = the_date;
  210. sd.score = stmt.getColumn(2);
  211. sd.won = stmt.getColumn(3);
  212. vsd.push_back(sd);
  213. }
  214. if (!vsd.empty()) {
  215. scores[current] = std::move(vsd);
  216. }
  217. vsd.clear();
  218. } catch (std::exception &e) {
  219. if (get_logger) {
  220. get_logger() << "getScores(): " << std::endl;
  221. get_logger() << "SQLite exception: " << e.what() << std::endl;
  222. }
  223. scores.clear();
  224. }
  225. return scores;
  226. }
  227. void DBData::expireScores(void) {}
  228. /**
  229. * @brief Format date to string.
  230. *
  231. * We use default "%0m/%0d/%Y", but can be configured by SysOp via
  232. * config["date_score"] setting. "%Y/%0m/%0d" for non-US?
  233. *
  234. * @param tt
  235. * @return std::string
  236. */
  237. std::string convertDateToDateScoreFormat(time_t tt) {
  238. std::stringstream ss;
  239. if (config["date_score"]) {
  240. std::string custom_format = config["date_score"].as<std::string>();
  241. ss << std::put_time(std::localtime(&tt), custom_format.c_str());
  242. } else {
  243. ss << std::put_time(std::localtime(&tt), "%0m/%0d/%Y");
  244. }
  245. std::string date = ss.str();
  246. return date;
  247. }
  248. /**
  249. * @brief change datetime to have consistent time
  250. *
  251. * This converts the time part to hour:00
  252. *
  253. * @param tt
  254. * @param hour
  255. */
  256. void normalizeDate(time_t &tt, int hour) {
  257. std::tm *local_tm = localtime(&tt);
  258. // adjust date to 2:00:00 AM
  259. tt -= (local_tm->tm_min * 60) + local_tm->tm_sec;
  260. while (local_tm->tm_hour < hour) {
  261. ++local_tm->tm_hour;
  262. tt += 60 * 60;
  263. }
  264. if (local_tm->tm_hour > hour) {
  265. tt -= (60 * 60) * (local_tm->tm_hour - hour);
  266. }
  267. /* // possible DST adjustment. LMTATSM
  268. if (local_tm->tm_isdst) {
  269. // DST in effect
  270. tt -= (60*60);
  271. }
  272. */
  273. }