/ firmware / src / services / http / api / database.cpp
database.cpp
  1  #include "api.h"
  2  
  3  #include <sqlite.h>
  4  #include <AsyncJson.h>
  5  #include <ArduinoJson.h>
  6  
  7  namespace {
  8  
  9  struct QueryContext {
 10    JsonArray columns;
 11    JsonArray rows;
 12    bool first_row;
 13  };
 14  
 15  int collect_row(void *ctx, int cols, char **values, char **names) {
 16    auto *qc = static_cast<QueryContext *>(ctx);
 17    if (qc->first_row) {
 18      for (int i = 0; i < cols; i++)
 19        qc->columns.add(names[i]);
 20      qc->first_row = false;
 21    }
 22    JsonArray row = qc->rows.add<JsonArray>();
 23    for (int i = 0; i < cols; i++)
 24      row.add(values[i] ? (const char *)values[i] : nullptr);
 25    return 0;
 26  }
 27  
 28  void handle_status(AsyncWebServerRequest *request) {
 29    AsyncJsonResponse *response = new AsyncJsonResponse();
 30    JsonObject root = response->getRoot().to<JsonObject>();
 31    root["ok"] = true;
 32    JsonObject data = root["data"].to<JsonObject>();
 33    data["open"] = programs::sqlite::isOpen();
 34    data["path"] = programs::sqlite::currentPath();
 35    if (programs::sqlite::lastErrorCode() != SQLITE_OK) {
 36      data["last_error_code"] = programs::sqlite::lastErrorCode();
 37      data["last_error"] = programs::sqlite::lastError();
 38    }
 39    data["sqlite_memory_used"] = (long long)programs::sqlite::memoryUsed();
 40    data["sqlite_memory_highwater"] = (long long)programs::sqlite::memoryHighwater();
 41    response->setLength();
 42    request->send(response);
 43  }
 44  
 45  void handle_tables(AsyncWebServerRequest *request) {
 46    if (!programs::sqlite::isOpen()) {
 47      request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"not open\"}");
 48      return;
 49    }
 50  
 51    AsyncJsonResponse *response = new AsyncJsonResponse();
 52    JsonObject root = response->getRoot().to<JsonObject>();
 53    JsonArray tables = root["data"].to<JsonArray>();
 54  
 55    struct TablesCtx { JsonArray *arr; };
 56    TablesCtx ctx = {&tables};
 57  
 58    char *err = nullptr;
 59    int rc = sqlite3_exec(programs::sqlite::handle(),
 60        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;",
 61        [](void *ctx, int cols, char **values, char **) -> int {
 62          auto *tc = static_cast<TablesCtx *>(ctx);
 63          if (cols > 0 && values[0]) tc->arr->add(values[0]);
 64          return 0;
 65        }, &ctx, &err);
 66  
 67    root["ok"] = (rc == SQLITE_OK);
 68    if (rc != SQLITE_OK) {
 69      root["error"] = err ? err : "unknown";
 70      sqlite3_free(err);
 71    }
 72    response->setLength();
 73    request->send(response);
 74  }
 75  
 76  }
 77  
 78  void services::http::api::database::registerRoutes(AsyncWebServer &server) {
 79    server.on("/api/database/status", HTTP_GET, handle_status);
 80    server.on("/api/database/tables", HTTP_GET, handle_tables);
 81  
 82    server.on("/api/database/close", HTTP_POST,
 83        [](AsyncWebServerRequest *request) {
 84      programs::sqlite::close();
 85      request->send(200, asyncsrv::T_application_json, "{\"ok\":true}");
 86    });
 87  
 88    AsyncCallbackJsonWebHandler &open_handler =
 89        server.on("/api/database/open", HTTP_POST,
 90            [](AsyncWebServerRequest *request, JsonVariant &json) {
 91      JsonObject body = json.as<JsonObject>();
 92      const char *path = body["path"] | (const char *)nullptr;
 93      bool ok = programs::sqlite::open(path);
 94  
 95      AsyncJsonResponse *response = new AsyncJsonResponse();
 96      JsonObject root = response->getRoot().to<JsonObject>();
 97      root["ok"] = ok;
 98      if (ok) {
 99        root["path"] = programs::sqlite::currentPath();
100      } else {
101        root["error"] = programs::sqlite::lastError();
102      }
103      response->setLength();
104      request->send(response);
105    });
106    open_handler.setMaxContentLength(256);
107  
108    AsyncCallbackJsonWebHandler &exec_handler =
109        server.on("/api/database/exec", HTTP_POST,
110            [](AsyncWebServerRequest *request, JsonVariant &json) {
111      if (!programs::sqlite::isOpen()) {
112        request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"not open\"}");
113        return;
114      }
115  
116      JsonObject body = json.as<JsonObject>();
117      const char *sql = body["sql"] | (const char *)nullptr;
118      if (!sql || sql[0] == '\0') {
119        request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"missing sql\"}");
120        return;
121      }
122  
123      AsyncJsonResponse *response = new AsyncJsonResponse();
124      JsonObject root = response->getRoot().to<JsonObject>();
125      JsonObject data = root["data"].to<JsonObject>();
126      JsonArray columns = data["columns"].to<JsonArray>();
127      JsonArray rows = data["rows"].to<JsonArray>();
128  
129      QueryContext qc = {columns, rows, true};
130      char *err = nullptr;
131      unsigned long start = micros();
132      int rc = sqlite3_exec(programs::sqlite::handle(), sql, collect_row, &qc, &err);
133      unsigned long elapsed = micros() - start;
134  
135      root["ok"] = (rc == SQLITE_OK);
136      data["elapsed_us"] = elapsed;
137      if (rc == SQLITE_OK) {
138        int changes = programs::sqlite::changes();
139        if (changes > 0) {
140          data["changes"] = changes;
141          data["last_insert_rowid"] = (long long)programs::sqlite::lastInsertRowid();
142        }
143      } else {
144        root["error"] = err ? err : "unknown";
145        root["error_code"] = sqlite3_extended_errcode(programs::sqlite::handle());
146        sqlite3_free(err);
147      }
148      response->setLength();
149      request->send(response);
150    });
151    exec_handler.setMaxContentLength(512);
152  }
153  
154  #ifdef PIO_UNIT_TESTING
155  
156  #include <testing/utils.h>
157  #include <storage.h>
158  #include <SD.h>
159  
160  static void test_db_collect_row_populates_columns_once(void) {
161    GIVEN("two rows are passed to collect_row");
162    THEN("column names are captured only on the first row");
163  
164    JsonDocument doc;
165    JsonArray columns = doc["columns"].to<JsonArray>();
166    JsonArray rows = doc["rows"].to<JsonArray>();
167    QueryContext qc = {columns, rows, true};
168  
169    char *names[] = {(char *)"id", (char *)"name"};
170    char *vals1[] = {(char *)"1", (char *)"alice"};
171    char *vals2[] = {(char *)"2", (char *)"bob"};
172  
173    collect_row(&qc, 2, vals1, names);
174    collect_row(&qc, 2, vals2, names);
175  
176    TEST_ASSERT_EQUAL_INT_MESSAGE(2, columns.size(),
177        "device: should have 2 column names");
178    TEST_ASSERT_EQUAL_STRING_MESSAGE("id", columns[0].as<const char *>(),
179        "device: first column should be 'id'");
180    TEST_ASSERT_EQUAL_STRING_MESSAGE("name", columns[1].as<const char *>(),
181        "device: second column should be 'name'");
182    TEST_ASSERT_EQUAL_INT_MESSAGE(2, rows.size(),
183        "device: should have 2 rows");
184    TEST_ASSERT_EQUAL_STRING("alice", rows[0][1].as<const char *>());
185    TEST_ASSERT_EQUAL_STRING("bob", rows[1][1].as<const char *>());
186  }
187  
188  static void test_db_collect_row_handles_null_values(void) {
189    WHEN("a SQL NULL value is passed to collect_row");
190    THEN("it serializes as JSON null");
191  
192    JsonDocument doc;
193    JsonArray columns = doc["columns"].to<JsonArray>();
194    JsonArray rows = doc["rows"].to<JsonArray>();
195    QueryContext qc = {columns, rows, true};
196  
197    char *names[] = {(char *)"v"};
198    char *vals[] = {nullptr};
199  
200    collect_row(&qc, 1, vals, names);
201  
202    TEST_ASSERT_EQUAL_INT(1, rows.size());
203    TEST_ASSERT_TRUE_MESSAGE(rows[0][0].isNull(),
204        "device: NULL value should serialize as JSON null");
205  }
206  
207  static void test_db_exec_roundtrip(void) {
208    GIVEN("a test database on SD");
209    WHEN("a table is created, a row inserted, and queried");
210    THEN("the results match");
211  
212    if (!programs::sqlite::open("/sd/test_db_api.db")) {
213      TEST_IGNORE_MESSAGE("skipped — could not open test database on SD");
214      return;
215    }
216    sqlite3 *h = programs::sqlite::handle();
217    char *err = nullptr;
218  
219    sqlite3_exec(h, "DROP TABLE IF EXISTS items;", nullptr, nullptr, nullptr);
220    sqlite3_exec(h, "CREATE TABLE items(id INTEGER PRIMARY KEY, label TEXT);",
221                 nullptr, nullptr, &err);
222    TEST_ASSERT_NULL(err);
223    sqlite3_exec(h, "INSERT INTO items(label) VALUES('sensor_a');",
224                 nullptr, nullptr, &err);
225    TEST_ASSERT_NULL(err);
226  
227    JsonDocument doc;
228    JsonArray columns = doc["columns"].to<JsonArray>();
229    JsonArray rows = doc["rows"].to<JsonArray>();
230    QueryContext qc = {columns, rows, true};
231  
232    int rc = sqlite3_exec(h, "SELECT id, label FROM items;", collect_row, &qc, &err);
233    TEST_ASSERT_EQUAL_INT_MESSAGE(SQLITE_OK, rc, "device: SELECT should succeed");
234    TEST_ASSERT_EQUAL_INT(2, columns.size());
235    TEST_ASSERT_EQUAL_INT(1, rows.size());
236    TEST_ASSERT_EQUAL_STRING("sensor_a", rows[0][1].as<const char *>());
237  
238    programs::sqlite::close();
239    SD.remove("/sd/test_db_api.db");
240  }
241  
242  void services::http::api::database::test(void) {
243    MODULE("DB");
244    RUN_TEST(test_db_collect_row_populates_columns_once);
245    RUN_TEST(test_db_collect_row_handles_null_values);
246    RUN_TEST(test_db_exec_roundtrip);
247  }
248  
249  #endif