1#include "core/aqm_db.h"
2#include "core/aqm_paths.h"
7static int exec_sql(sqlite3 *db,
const char *sql) {
9 int rc = sqlite3_exec(db, sql, NULL, NULL, &err);
10 if (rc != SQLITE_OK) {
11 fprintf(stderr,
"SQLite error: %s\n", err ? err : sqlite3_errmsg(db));
18static int table_exists(sqlite3 *db,
const char *name) {
19 sqlite3_stmt *st = NULL;
20 const char *sql =
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?;";
21 if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK)
23 sqlite3_bind_text(st, 1, name, -1, SQLITE_STATIC);
24 int exists = sqlite3_step(st) == SQLITE_ROW;
29static int column_exists(sqlite3 *db,
const char *table,
const char *column) {
30 sqlite3_stmt *st = NULL;
32 int n = snprintf(sql,
sizeof(sql),
"PRAGMA table_info(%s);", table);
33 if (n < 0 || (
size_t)n >=
sizeof(sql))
35 if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK)
38 while (sqlite3_step(st) == SQLITE_ROW) {
39 const char *name = (
const char *)sqlite3_column_text(st, 1);
40 if (name && strcmp(name, column) == 0) {
49static int migrate_legacy_sensor_data(sqlite3 *db) {
50 if (!table_exists(db,
"SensorData"))
53 if (exec_sql(db,
"PRAGMA foreign_keys = OFF;") != 0)
57 "INSERT OR IGNORE INTO sensors (id, name) "
58 "SELECT DISTINCT sensor_id, 'Sensor ' || CAST(sensor_id AS TEXT) FROM SensorData;") != 0) {
59 exec_sql(db,
"PRAGMA foreign_keys = ON;");
64 "INSERT INTO readings (sensor_id, measured_at, model, pm25, pm10, co, no2, o3, so2) "
65 "SELECT sensor_id, timestamp, 'legacy', pm25, pm10, co, no2, o3, so2 FROM SensorData;") != 0) {
66 exec_sql(db,
"PRAGMA foreign_keys = ON;");
70 if (exec_sql(db,
"DROP TABLE IF EXISTS SensorData;") != 0) {
71 exec_sql(db,
"PRAGMA foreign_keys = ON;");
75 if (exec_sql(db,
"PRAGMA foreign_keys = ON;") != 0)
78 printf(
"Migrated legacy table SensorData into normalized schema.\n");
87int aqm_db_get_path(
char *buf,
size_t buflen) {
return aqm_get_db_path(buf, buflen); }
89int aqm_db_open(sqlite3 **out_db) {
92 char path[AQM_PATH_MAX];
93 if (aqm_get_db_path(path,
sizeof(path)) != 0) {
94 fprintf(stderr,
"Could not resolve database path.\n");
98 if (sqlite3_open(path, &db) != SQLITE_OK || !db) {
99 const char *errmsg = db ? sqlite3_errmsg(db) :
"Failed to allocate database handle";
100 fprintf(stderr,
"Cannot open database: %s\n", errmsg);
105 sqlite3_busy_timeout(db, 5000);
106 exec_sql(db,
"PRAGMA foreign_keys = ON;");
111void aqm_db_close(sqlite3 *db) {
116int aqm_db_init(
void) {
118 if (aqm_db_open(&db) != 0)
121 const char *parts[] = {
122 "CREATE TABLE IF NOT EXISTS sensors ("
123 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
124 " name TEXT NOT NULL"
126 "CREATE TABLE IF NOT EXISTS readings ("
127 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
128 " sensor_id INTEGER NOT NULL,"
129 " measured_at TEXT NOT NULL,"
130 " model TEXT NOT NULL DEFAULT '',"
131 " pm25 REAL NOT NULL,"
132 " pm10 REAL NOT NULL,"
134 " no2 REAL NOT NULL,"
136 " so2 REAL NOT NULL,"
137 " FOREIGN KEY (sensor_id) REFERENCES sensors(id),"
138 " UNIQUE (sensor_id, measured_at)"
140 "CREATE INDEX IF NOT EXISTS idx_readings_time ON readings(measured_at);",
141 "CREATE INDEX IF NOT EXISTS idx_readings_sensor ON readings(sensor_id);",
144 for (
size_t i = 0; i <
sizeof(parts) /
sizeof(parts[0]); i++) {
145 if (exec_sql(db, parts[i]) != 0) {
151 if (!column_exists(db,
"readings",
"model")) {
152 if (exec_sql(db,
"ALTER TABLE readings ADD COLUMN model TEXT NOT NULL DEFAULT '';") != 0) {
159 "SET model = CASE WHEN model IS NULL OR model = '' THEN "
160 "COALESCE((SELECT s.name FROM sensors s WHERE s.id = readings.sensor_id), 'unknown') "
161 "ELSE model END;") != 0) {
166 if (migrate_legacy_sensor_data(db) != 0) {
171 sqlite3_stmt *ins = NULL;
172 const char *ensure =
"INSERT OR IGNORE INTO sensors (id, name) VALUES (1, 'Default sensor');";
173 if (sqlite3_prepare_v2(db, ensure, -1, &ins, NULL) != SQLITE_OK) {
174 fprintf(stderr,
"Prepare error: %s\n", sqlite3_errmsg(db));
179 sqlite3_finalize(ins);
181 char path[AQM_PATH_MAX];
182 if (aqm_get_db_path(path,
sizeof(path)) == 0)
183 printf(
"Database ready at %s\n", path);