use std::path::Path; use anyhow::Result; use rusqlite::{Connection, ToSql}; use crate::devices::{Devices, DevicesWithName}; pub struct DataBase { sql: Connection, } impl DataBase { const VERSION_0_1_0: &'static str = "0.1.0"; pub async fn new(path: impl AsRef) -> Result { let me = Self { sql: Connection::open(path)?, }; me.generate_tables()?; me.init()?; Ok(me) } fn generate_tables(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS meta ( id INTEGER PRIMARY KEY, version INTEGER NOT NULL )", [], )?; self.sql.execute( "CREATE TABLE IF NOT EXISTS devices( id INTEGER PRIMARY KEY, device VARCHAR(60) NOT NULL, type VARCHAR(30) NOT NULL, name VARCHAR(80) )", [], )?; self.sql.execute( "CREATE TABLE IF NOT EXISTS data ( id INTEGER PRIMARY KEY, time BIGINT NOT NULL, watts REAL NOT NULL, device_id INTEGER NOT NULL, FOREIGN KEY(device_id) REFERENCES devices(id) )", [], )?; Ok(()) } pub fn version(&self) -> Result { Ok(self .sql .query_row("SELECT version FROM meta WHERE id=1", [], |row| row.get(0))?) } fn init(&self) -> Result<()> { if self.version().is_err() { self.sql.execute( "INSERT INTO meta (version) VALUES (?1)", &[Self::VERSION_0_1_0], )?; } Ok(()) } pub fn register_devices(&self, devices: &Devices) -> Result<()> { for device in devices.plugs.iter() { self.sql.execute( "INSERT INTO devices (device, type) SELECT ?1, \"plug\" WHERE NOT EXISTS ( SELECT device FROM devices WHERE device=\"?1\" ) ", &[device], )?; } Ok(()) } pub fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> { let params: &[&dyn ToSql] = &[&time, &watts, &device_name]; self.sql.execute( &format!( "INSERT INTO data (time, watts, device_id) VALUES (?1, ?2, (SELECT id FROM devices WHERE device=?3) )" ), params, )?; Ok(()) } pub fn devices(&self) -> Result { let mut devices = DevicesWithName::default(); for row in self .sql .prepare(&format!( " SELECT device, type, name FROM devices " ))? .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))? { let (device, dev_type, name): (String, String, String) = row?; match dev_type.as_str() { "plug" => devices.plugs.push((device, name)), _ => panic!(), } } Ok(devices) } pub fn change_device_name(&self, device: &str, description: &str) -> Result<()> { self.sql.execute( &format!( " UPDATE devices SET name=?1 WHERE device=?2 " ), &[&description, device], )?; Ok(()) } pub fn read(&self, device: &str) -> Result> { self.sql .prepare(&format!( " SELECT time, watts FROM data WHERE device=\"{device}\" " ))? .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? .map(|row| { let (time, watts) = row?; Ok((time, watts)) }) .collect() } } #[cfg(test)] mod test { use std::fs; use anyhow::Result; use crate::devices::Devices; use super::DataBase; #[tokio::test] async fn test_connection() -> Result<()> { let db = DataBase::new("connection_test.db").await?; assert_eq!(DataBase::VERSION_0_1_0, db.version()?); fs::remove_file("connection_test.db")?; Ok(()) } #[tokio::test] async fn test_startup() -> Result<()> { let db = DataBase::new("startup_test.db").await?; db.register_devices(&Devices { plugs: vec!["test".to_string()], })?; fs::remove_file("startup_test.db")?; Ok(()) } #[tokio::test] async fn test_write() -> Result<()> { let db = DataBase::new("write_test.db").await?; let device_name = "test"; db.register_devices(&Devices { plugs: vec![device_name.to_string()], })?; db.write(device_name, 0, 5.5)?; db.change_device_name(device_name, "udo")?; fs::remove_file("write_test.db")?; Ok(()) } }