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:
parent
116e055e6a
commit
48d93321a3
99
LORAWEB_NEU/backend/daemon/src/db.rs
Normal file
99
LORAWEB_NEU/backend/daemon/src/db.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
LORAWEB_NEU/database/migrations/001_initial.sql
Normal file
33
LORAWEB_NEU/database/migrations/001_initial.sql
Normal 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
|
||||||
|
);
|
||||||
Loading…
x
Reference in New Issue
Block a user