db.c 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /*
  2. * Copyright (c) 2018 Markus Hennecke <markus-hennecke@markus-hennecke.de>
  3. *
  4. * Permission to use, copy, modify, and distribute this software for any
  5. * purpose with or without fee is hereby granted, provided that the above
  6. * copyright notice and this permission notice appear in all copies.
  7. *
  8. * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  9. * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10. * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11. * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12. * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13. * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14. * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. */
  16. #include <sys/types.h>
  17. #include <err.h>
  18. #include <stdarg.h>
  19. #include <stdint.h>
  20. #include <stdlib.h>
  21. #include <string.h>
  22. #include <kcgi.h>
  23. #include "db.h"
  24. #include <postgresql/libpq-fe.h>
  25. static PGconn *conn = NULL;
  26. static bool atexit_regdone = false;
  27. static bool _exec_in_tx(struct kreq *, const char *, const char * const *,
  28. const size_t);
  29. static bool _query(struct kreq *, char **, const char *, const char *,
  30. const char *);
  31. bool
  32. db_connect(struct kreq *r)
  33. {
  34. if (conn)
  35. return true;
  36. conn = PQconnectdb(
  37. "postgresql://" DATABASE_USER ":" DATABASE_PASSWORD
  38. "@" DATABASE_HOSTNAME ":" DATABASE_PORT "/weatherdata"
  39. "?application_name=weatherdata_input");
  40. if (PQstatus(conn) != CONNECTION_OK) {
  41. kutil_warnx(r, NULL, "Connection to database failed: %s",
  42. PQerrorMessage(conn));
  43. }
  44. if (!atexit_regdone)
  45. atexit_regdone = (atexit(db_disconnect) == 0);
  46. return (PQstatus(conn) == CONNECTION_OK);
  47. }
  48. void
  49. db_disconnect(void)
  50. {
  51. if (conn) {
  52. PQfinish(conn);
  53. conn = NULL;
  54. }
  55. }
  56. bool
  57. _exec_in_tx(struct kreq *r, const char *sql, const char * const params[],
  58. const size_t n)
  59. {
  60. if (! db_connect(r))
  61. return false;
  62. PGresult *res = PQexec(conn, "BEGIN");
  63. if (PQresultStatus(res) != PGRES_COMMAND_OK) {
  64. kutil_warnx(r, NULL, "BEGIN command failed: %s",
  65. PQerrorMessage(conn));
  66. PQclear(res);
  67. return false;
  68. }
  69. PQclear(res);
  70. res = PQexecParams(conn, sql, n, NULL, params, NULL, NULL, 0);
  71. if (PQresultStatus(res) != PGRES_COMMAND_OK) {
  72. kutil_warnx(r, NULL, "stmt \"%s\" failed: %s", sql,
  73. PQerrorMessage(conn));
  74. PQclear(res);
  75. return false;
  76. }
  77. PQclear(res);
  78. res = PQexec(conn, "COMMIT");
  79. if (PQresultStatus(res) != PGRES_COMMAND_OK) {
  80. kutil_warnx(r, NULL, "COMMIT command failed: %s",
  81. PQerrorMessage(conn));
  82. PQclear(res);
  83. return false;
  84. }
  85. PQclear(res);
  86. return true;
  87. }
  88. bool
  89. db_add_error(struct kreq *r, const char *device, const char *timestamp,
  90. const char *errormsg)
  91. {
  92. if (timestamp == NULL) {
  93. const char * const params[2] = { device, errormsg };
  94. const char * sql = "INSERT INTO \"errors\" ("
  95. "\"device\", \"error\") VALUES ($1, $2)";
  96. return _exec_in_tx(r, sql, params, 2);
  97. } else {
  98. const char * const params[3] = { device, timestamp, errormsg };
  99. const char * sql = "INSERT INTO \"errors\" ("
  100. "\"device\", \"timestamp\", \"error\""
  101. ") VALUES ($1, $2, $3)";
  102. return _exec_in_tx(r, sql, params, 3);
  103. }
  104. }
  105. bool
  106. db_add_data(struct kreq *r, const char *temp, const char *humidity,
  107. const char *pressure, const char *battery)
  108. {
  109. const char * const params[4] = { temp, humidity, pressure, battery };
  110. const char * sql = "INSERT INTO \"data\" ("
  111. "\"timestamp\", \"temp\", \"humidity\", \"pressure\", \"battery\") "
  112. "VALUES (NOW(), $1, $2, $3, $4)";
  113. return _exec_in_tx(r, sql, params, 4);
  114. }
  115. /*
  116. * Accepts SQL query strings and replaces %s%s%s with a WHERE statement
  117. * depending on the passed _from and _until parameters. If both parameters
  118. * are not NULL the WHERE statement looks like this:
  119. * WHERE (timestamp >= $1::timestamptz AND timestamp < $2::timestamptz)
  120. * For only one parameter being not NULL corresponding part of the
  121. * statement above is used alone.
  122. * If both _from and _until are NULL an empty string will be used.
  123. */
  124. static bool
  125. _query(struct kreq *r, char **_json, const char *_sqlfmt, const char *_from,
  126. const char *_until)
  127. {
  128. if (! db_connect(r))
  129. return false;
  130. int num_params = 0;
  131. if (_from)
  132. num_params += 1;
  133. if (_until)
  134. num_params += 1;
  135. const char *where = (num_params != 0) ? " WHERE (" : "";
  136. const char *from = (_from) ? "\"timestamp\" >= $1::timestamptz" : "";
  137. const char *until =
  138. (_until) ? (
  139. (num_params == 2) ? " AND \"timestamp\" < $2::timestamptz)"
  140. : "\"timestamp\" < $1::timestamptz)")
  141. : (num_params == 1) ? ")"
  142. : "";
  143. char *sql;
  144. if (asprintf(&sql, _sqlfmt, where, from, until) == -1) {
  145. kutil_warnx(r, NULL, "Unable to create query string");
  146. return false;
  147. }
  148. //kutil_info(r, NULL, "query = %s", sql);
  149. const char * const params[2] = {
  150. ((num_params == 1) && _until) ? _until: _from,
  151. _until
  152. };
  153. PGresult *res = PQexecParams(conn, sql, num_params, NULL, params,
  154. NULL, NULL, 0);
  155. if (PQresultStatus(res) != PGRES_TUPLES_OK) {
  156. kutil_warnx(r, NULL, "Query %s failed: %s", sql,
  157. PQerrorMessage(conn));
  158. PQclear(res);
  159. free(sql);
  160. return false;
  161. }
  162. free(sql);
  163. const int rows = PQntuples(res);
  164. if (rows == 0) {
  165. *_json = strdup("[]");
  166. PQclear(res);
  167. return true;
  168. } else if (rows != 1) {
  169. kutil_warnx(r, NULL, "Got %d rows, no json?", rows);
  170. PQclear(res);
  171. return false;
  172. }
  173. *_json = strdup(PQgetvalue(res, 0, 0));
  174. PQclear(res);
  175. return true;
  176. }
  177. bool
  178. db_get_statistics_data_json(struct kreq *r, char **_json, const char *_from,
  179. const char *_until)
  180. {
  181. return _query(r, _json,
  182. "SELECT coalesce(row_to_json(\"d\"), '[]'::json)"
  183. " AS \"data\" "
  184. "FROM "
  185. "(WITH \"statistics\" AS ("
  186. "SELECT \"timestamp\"::date,"
  187. " round(min(\"temp\")::numeric, 2) AS \"min_temp\","
  188. " round(max(\"temp\")::numeric, 2) AS \"max_temp\","
  189. " round(avg(\"humidity\")::numeric, 2) AS \"avg_humidity\" "
  190. "FROM \"data\" WHERE \"timestamp\"::date IN ("
  191. "SELECT DISTINCT ON (\"timestamp\"::date) "
  192. "\"timestamp\"::date FROM \"data\" "
  193. "%s%s%s) "
  194. "GROUP BY \"timestamp\"::date "
  195. "ORDER BY \"timestamp\"::date ASC) "
  196. "SELECT array_agg(\"timestamp\") AS \"labels\", "
  197. " array_agg(\"min_temp\") AS \"min_temp\","
  198. " array_agg(\"max_temp\") AS \"max_temp\","
  199. " array_agg(\"avg_humidity\") AS \"avg_humidity\" "
  200. "FROM \"statistics\") \"d\"", _from, _until);
  201. }
  202. bool
  203. db_get_data_json(struct kreq *r, char **_json, const char *_from,
  204. const char *_until)
  205. {
  206. return _query(r, _json,
  207. "SELECT coalesce(row_to_json(\"d\"), '[]'::json)"
  208. " AS \"data\" "
  209. "FROM "
  210. "(WITH \"weatherdata\" AS ("
  211. "SELECT \"timestamp\", \"temp\", \"humidity\","
  212. " \"pressure\", \"battery\" FROM \"data\" %s%s%s "
  213. "ORDER BY \"timestamp\" ASC) "
  214. "SELECT array_agg(\"timestamp\") AS \"labels\", "
  215. " array_agg(\"temp\") AS \"temp\", "
  216. " array_agg(\"humidity\") AS \"humidity\", "
  217. " array_agg(\"pressure\") AS \"pressure\", "
  218. " array_agg(\"battery\") AS \"battery\" "
  219. "FROM \"weatherdata\") \"d\"", _from, _until);
  220. }
  221. bool
  222. db_get_latest_json(struct kreq *r, char **_json, const char *_from_unused,
  223. const char *_until_unused)
  224. {
  225. return _query(r, _json,
  226. "SELECT coalesce(row_to_json(\"d\"), '[]'::json)"
  227. " AS \"data\" "
  228. "FROM "
  229. "(SELECT date_trunc('second', \"timestamp\") as \"timestamp\","
  230. " \"avg_temp\", \"avg_humidity\", \"temp\","
  231. " \"humidity\", \"pressure\", \"battery\" FROM ("
  232. "SELECT 'j' AS j,"
  233. " round(avg(\"temp\")::numeric, 2) AS \"avg_temp\", "
  234. " round(avg(\"humidity\")::numeric, 2) AS \"avg_humidity\" "
  235. "FROM \"data\" "
  236. "WHERE \"timestamp\" >= now() - '1 hour'::interval"
  237. ") AS \"averages\" "
  238. "JOIN"
  239. " (SELECT 'j' AS \"j\", \"temp\", \"humidity\","
  240. " \"pressure\", \"battery\", \"timestamp\" "
  241. "FROM \"data\" "
  242. "WHERE \"timestamp\" = "
  243. "(SELECT max(\"timestamp\") FROM \"data\"))"
  244. " AS \"latest\" ON \"averages\".\"j\" = \"latest\".\"j\") \"d\"",
  245. NULL, NULL);
  246. }
  247. bool
  248. db_get_errors_json(struct kreq *r, char **json, const char *device)
  249. {
  250. if (! db_connect(r))
  251. return false;
  252. const char *select = "SELECT coalesce(jsonb_agg(\"e\"), '[]'::jsonb)"
  253. " AS \"errors\" "
  254. "FROM "
  255. "(SELECT "
  256. " date_trunc('second', \"timestamp\") AS \"timestamp\","
  257. " \"device\", \"error\" FROM \"errors\") AS \"e\"";
  258. const char *where = (device == NULL) ? "" : " WHERE \"device\" = ?";
  259. char *sql;
  260. if (-1 == asprintf(&sql, "%s%s", select, where)) {
  261. kutil_warnx(r, NULL, "Unable to create query string");
  262. return false;
  263. }
  264. const size_t num_params = (device != NULL) ? 1 : 0;
  265. //kutil_info(r, NULL, "query = %s", sql);
  266. const char * const params[1] = {
  267. device,
  268. };
  269. PGresult *res = PQexecParams(conn, sql, num_params, NULL, params,
  270. NULL, NULL, 0);
  271. if (PQresultStatus(res) != PGRES_TUPLES_OK) {
  272. kutil_warnx(r, NULL, "Query %s failed: %s", sql,
  273. PQerrorMessage(conn));
  274. PQclear(res);
  275. free(sql);
  276. return false;
  277. }
  278. free(sql);
  279. const int rows = PQntuples(res);
  280. if (rows == 0) {
  281. *json = strdup("[]");
  282. PQclear(res);
  283. return true;
  284. } else if (rows != 1) {
  285. kutil_warnx(r, NULL, "Got %d rows, no json?", rows);
  286. PQclear(res);
  287. return false;
  288. }
  289. *json = strdup(PQgetvalue(res, 0, 0));
  290. PQclear(res);
  291. return true;
  292. }