feat: add SQLite migration and DB init with WAL mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Mueller 2026-03-19 22:51:17 +01:00
parent 116e055e6a
commit 48d93321a3
2 changed files with 132 additions and 0 deletions

View File

@ -0,0 +1,99 @@
// backend/daemon/src/db.rs
use sqlx::SqlitePool;
use chrono::Utc;
use uuid::Uuid;
pub async fn init_db(pool: &SqlitePool) -> Result<(), sqlx::Error> {
sqlx::query("PRAGMA journal_mode=WAL").execute(pool).await?;
sqlx::query("PRAGMA busy_timeout=5000").execute(pool).await?;
let migration = include_str!("../../../database/migrations/001_initial.sql");
for statement in migration.split(';') {
let stmt = statement.trim();
if !stmt.is_empty() {
sqlx::query(stmt).execute(pool).await?;
}
}
Ok(())
}
pub async fn register_topic(pool: &SqlitePool, dev_eui: &str) -> Result<(), sqlx::Error> {
let now = Utc::now().to_rfc3339();
let id = Uuid::new_v4().to_string();
sqlx::query(
"INSERT OR IGNORE INTO topics (id, topic, approved, alarm_status, alarm_threshold_hours, created_at) \
VALUES (?, ?, 0, 'unknown', 0.0, ?)"
)
.bind(&id)
.bind(dev_eui)
.bind(&now)
.execute(pool)
.await?;
Ok(())
}
pub async fn update_last_seen(pool: &SqlitePool, dev_eui: &str, timestamp: &str) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE topics SET last_seen = ? WHERE topic = ?")
.bind(timestamp)
.bind(dev_eui)
.execute(pool)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use sqlx::SqlitePool;
#[tokio::test]
async fn test_init_db_creates_tables() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
init_db(&pool).await.unwrap();
let topics: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM topics")
.fetch_one(&pool).await.unwrap();
assert_eq!(topics.0, 0);
let sensors: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM sensors")
.fetch_one(&pool).await.unwrap();
assert_eq!(sensors.0, 0);
let users: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(&pool).await.unwrap();
assert_eq!(users.0, 0);
}
#[tokio::test]
async fn test_init_db_is_idempotent() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
init_db(&pool).await.unwrap();
init_db(&pool).await.unwrap(); // should not error
}
#[tokio::test]
async fn test_register_topic() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
init_db(&pool).await.unwrap();
register_topic(&pool, "abcdef1234567890").await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM topics WHERE topic = ?")
.bind("abcdef1234567890")
.fetch_one(&pool).await.unwrap();
assert_eq!(count.0, 1);
}
#[tokio::test]
async fn test_register_topic_idempotent() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
init_db(&pool).await.unwrap();
register_topic(&pool, "abcdef1234567890").await.unwrap();
register_topic(&pool, "abcdef1234567890").await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM topics")
.fetch_one(&pool).await.unwrap();
assert_eq!(count.0, 1);
}
}

View File

@ -0,0 +1,33 @@
-- database/migrations/001_initial.sql
CREATE TABLE IF NOT EXISTS topics (
id TEXT PRIMARY KEY,
topic TEXT UNIQUE NOT NULL,
approved INTEGER NOT NULL DEFAULT 0,
last_seen TEXT,
alarm_status TEXT NOT NULL DEFAULT 'unknown',
alarm_threshold_hours REAL NOT NULL DEFAULT 0.0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sensors (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
location TEXT NOT NULL DEFAULT '',
sensor_type TEXT NOT NULL DEFAULT '',
active INTEGER NOT NULL DEFAULT 1,
topic_id TEXT REFERENCES topics(id),
display_config TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
totp_secret TEXT,
totp_enabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);