Air Quality Monitor
Portable C application for collecting, storing, and analyzing air quality sensor readings
Loading...
Searching...
No Matches
aqm_db.c
1#include "core/aqm_db.h"
2#include "core/aqm_paths.h"
3#include <stdio.h>
4#include <stdlib.h>
5#include <string.h>
6
7static int exec_sql(sqlite3 *db, const char *sql) {
8 char *err = NULL;
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));
12 sqlite3_free(err);
13 return -1;
14 }
15 return 0;
16}
17
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)
22 return 0;
23 sqlite3_bind_text(st, 1, name, -1, SQLITE_STATIC);
24 int exists = sqlite3_step(st) == SQLITE_ROW;
25 sqlite3_finalize(st);
26 return exists;
27}
28
29static int column_exists(sqlite3 *db, const char *table, const char *column) {
30 sqlite3_stmt *st = NULL;
31 char sql[128];
32 int n = snprintf(sql, sizeof(sql), "PRAGMA table_info(%s);", table);
33 if (n < 0 || (size_t)n >= sizeof(sql))
34 return 0;
35 if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK)
36 return 0;
37 int found = 0;
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) {
41 found = 1;
42 break;
43 }
44 }
45 sqlite3_finalize(st);
46 return found;
47}
48
49static int migrate_legacy_sensor_data(sqlite3 *db) {
50 if (!table_exists(db, "SensorData"))
51 return 0;
52
53 if (exec_sql(db, "PRAGMA foreign_keys = OFF;") != 0)
54 return -1;
55
56 if (exec_sql(db,
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;");
60 return -1;
61 }
62
63 if (exec_sql(db,
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;");
67 return -1;
68 }
69
70 if (exec_sql(db, "DROP TABLE IF EXISTS SensorData;") != 0) {
71 exec_sql(db, "PRAGMA foreign_keys = ON;");
72 return -1;
73 }
74
75 if (exec_sql(db, "PRAGMA foreign_keys = ON;") != 0)
76 return -1;
77
78 printf("Migrated legacy table SensorData into normalized schema.\n");
79 return 0;
80}
81
82// public accessor for the resolved database path, exposed for external
83// tools/plugins that need to locate the .db file without duplicating
84// aqm_get_db_path's platform-specific resolution logic. not currently
85// called internally
86
87int aqm_db_get_path(char *buf, size_t buflen) { return aqm_get_db_path(buf, buflen); }
88
89int aqm_db_open(sqlite3 **out_db) {
90 if (!out_db)
91 return -1;
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");
95 return -1;
96 }
97 sqlite3 *db = NULL;
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);
101 if (db)
102 sqlite3_close(db);
103 return -1;
104 }
105 sqlite3_busy_timeout(db, 5000);
106 exec_sql(db, "PRAGMA foreign_keys = ON;");
107 *out_db = db;
108 return 0;
109}
110
111void aqm_db_close(sqlite3 *db) {
112 if (db)
113 sqlite3_close(db);
114}
115
116int aqm_db_init(void) {
117 sqlite3 *db = NULL;
118 if (aqm_db_open(&db) != 0)
119 return -1;
120
121 const char *parts[] = {
122 "CREATE TABLE IF NOT EXISTS sensors ("
123 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
124 " name TEXT NOT NULL"
125 ");",
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,"
133 " co REAL NOT NULL,"
134 " no2 REAL NOT NULL,"
135 " o3 REAL NOT NULL,"
136 " so2 REAL NOT NULL,"
137 " FOREIGN KEY (sensor_id) REFERENCES sensors(id),"
138 " UNIQUE (sensor_id, measured_at)"
139 ");",
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);",
142 };
143
144 for (size_t i = 0; i < sizeof(parts) / sizeof(parts[0]); i++) {
145 if (exec_sql(db, parts[i]) != 0) {
146 aqm_db_close(db);
147 return -1;
148 }
149 }
150
151 if (!column_exists(db, "readings", "model")) {
152 if (exec_sql(db, "ALTER TABLE readings ADD COLUMN model TEXT NOT NULL DEFAULT '';") != 0) {
153 aqm_db_close(db);
154 return -1;
155 }
156 }
157 if (exec_sql(db,
158 "UPDATE readings "
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) {
162 aqm_db_close(db);
163 return -1;
164 }
165
166 if (migrate_legacy_sensor_data(db) != 0) {
167 aqm_db_close(db);
168 return -1;
169 }
170
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));
175 aqm_db_close(db);
176 return -1;
177 }
178 sqlite3_step(ins);
179 sqlite3_finalize(ins);
180
181 char path[AQM_PATH_MAX];
182 if (aqm_get_db_path(path, sizeof(path)) == 0)
183 printf("Database ready at %s\n", path);
184
185 aqm_db_close(db);
186 return 0;
187}