aphomie_db_client 0.3.1

Common DTOs for communication over network for the BMS Uni project
Documentation
use anyhow::bail;
use bms_dtos::TrendDataRaw;
use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
use serde::{Deserialize, Serialize, ser::SerializeStruct};
use tokio_postgres::{Client, Config, NoTls, Row};

type Result<T> = core::result::Result<T, anyhow::Error>;

pub struct AphomieDbClient {
    db_client: Client,
}

impl AphomieDbClient {
    pub async fn new(host: Option<&str>, port: Option<u16>) -> Result<Self> {
        let host = if let Some(v) = host { v } else { "localhost" };

        let port = if let Some(v) = port { v } else { 5432 };

        let mut config = Config::new();
        config
            .user("dat7")
            .password("dat7")
            .dbname("dat7")
            .host(host)
            .port(port);

        let (db_client, connection) = config.connect(NoTls).await?;

        tokio::spawn(async move {
            if let Err(e) = connection.await {
                eprintln!("connection error: {}", e);
            }
        });

        Ok(Self { db_client })
    }

    pub async fn insert_metadata(&self, metadata: &MetadataDb) -> Result<Vec<MetadataDb>> {
        insert_metadata(
            &self.db_client,
            &metadata.log_id,
            &metadata.source,
            &metadata.unit,
            &metadata.alias,
        )
        .await
        .map(|v| v.iter().map(|r| MetadataDb::from(r)).collect())
    }

    pub async fn get_metadata_by_source(&self, source: &str) -> Result<Vec<MetadataDb>> {
        get_metadata_by_source(&self.db_client, source)
            .await
            .map(|v| v.iter().map(|r| MetadataDb::from(r)).collect())
    }

    pub async fn get_all_metadata(&self) -> Result<Vec<MetadataDb>> {
        get_all_metadata(&self.db_client)
            .await
            .map(|v| v.iter().map(|r| MetadataDb::from(r)).collect())
    }

    pub async fn insert_trend_data(&self, trend_data: &TrendDataDb) -> Result<Vec<TrendDataDb>> {
        insert_trend_data(
            &self.db_client,
            &trend_data.log_id,
            &trend_data.timestamp,
            &trend_data.value,
        )
        .await
        .map(|v| v.iter().map(|r| TrendDataDb::from(r)).collect())
    }

    pub async fn get_trend_data_by_period(
        &self,
        start: &DateTime<Utc>,
        end: &DateTime<Utc>,
    ) -> Result<Vec<TrendDataDb>> {
        get_trend_data_by_period(&self.db_client, start, end)
            .await
            .map(|v| v.iter().map(|r| TrendDataDb::from(r)).collect())
    }

    pub async fn get_trend_data_by_source_offset_limit(
        &self,
        source: &str,
        offset: i64,
        limit: i64,
    ) -> Result<Vec<MetadataTrendDataJoinDb>> {
        get_trend_data_by_source_offset_limit(&self.db_client, source, offset, limit)
            .await
            .map(|v| v.iter().map(|r| MetadataTrendDataJoinDb::from(r)).collect())
    }

    pub async fn get_metadata_join_trend_data_offset_limit(
        &self,
        offset: i64,
        limit: i64,
    ) -> Result<Vec<MetadataTrendDataJoinDb>> {
        get_metadata_join_trend_data_offset_limit(&self.db_client, offset, limit)
            .await
            .map(|v| v.iter().map(|r| MetadataTrendDataJoinDb::from(r)).collect())
    }

    pub async fn get_latest_trend_data(&self) -> Result<Option<TrendDataDb>> {
        match get_latest_trend_data(&self.db_client)
            .await
            .map(|v| v.iter().map(|e| TrendDataDb::from(e)).collect::<Vec<TrendDataDb>>())
        {
            Ok(v) => Ok(v.get(0).map(|e| e.clone())),
            Err(e) => bail!(e),
        }
    }
}

async fn insert_metadata(
    client: &Client,
    log_id: &i64,
    source: &str,
    unit: &str,
    alias: &Option<String>,
) -> Result<Vec<Row>> {
    let stmt =
        "INSERT INTO metadata (log_id, source, unit, alias) VALUES ($1, $2, $3, $4) RETURNING *;";

    match client.query(stmt, &[log_id, &source, &unit, &alias]).await {
        Ok(v) => Ok(v),
        Err(e) => {
            bail!("error querying database: {e}");
        }
    }
}

async fn get_metadata_by_source(client: &Client, source: &str) -> Result<Vec<Row>> {
    let stmt = "SELECT * FROM metadata WHERE source = $1;";

    match client.query(stmt, &[&source]).await {
        Ok(v) => Ok(v),
        Err(e) => {
            bail!("error querying database: {e}");
        }
    }
}

async fn get_all_metadata(client: &Client) -> Result<Vec<Row>> {
    let stmt = "SELECT * FROM metadata;";

    match client.query(stmt, &[]).await {
        Ok(v) => Ok(v),
        Err(e) => {
            bail!("error querying database: {e}");
        }
    }
}

async fn get_trend_data_by_period(
    client: &Client,
    start: &DateTime<Utc>,
    end: &DateTime<Utc>,
) -> Result<Vec<Row>> {
    let stmt = "SELECT * FROM trend_data WHERE timestamp <= $1 AND timestamp >= $2;";

    match client.query(stmt, &[&end, &start]).await {
        Ok(v) => Ok(v),
        Err(e) => {
            bail!("error querying database: {e}");
        }
    }
}

async fn get_trend_data_by_source_offset_limit(
    client: &Client,
    source: &str,
    offset: i64,
    limit: i64,
) -> Result<Vec<Row>> {
    let stmt = "SELECT t.log_id, t.timestamp, t.value, m.source, m.unit FROM metadata m JOIN trend_data t ON m.log_id = t.log_id WHERE source LIKE $1 ORDER BY t.log_id, t.timestamp OFFSET $2 LIMIT $3;";

    match client.query(stmt, &[&source, &offset, &limit]).await {
        Ok(v) => Ok(v),
        Err(e) => {
            bail!("error querying database: {e}");
        }
    }
}

async fn get_metadata_join_trend_data_offset_limit(
    client: &Client,
    offset: i64,
    limit: i64,
) -> Result<Vec<Row>> {
    let stmt = "SELECT t.log_id, t.timestamp, t.value, m.source, m.unit FROM metadata m JOIN trend_data t ON m.log_id = t.log_id ORDER BY t.timestamp, t.log_id OFFSET $1 LIMIT $2;";

    match client.query(stmt, &[&offset, &limit]).await {
        Ok(v) => Ok(v),
        Err(e) => {
            bail!("error querying database: {e}");
        }
    }
}

async fn get_latest_trend_data(client: &Client) -> Result<Vec<Row>> {
    let stmt = "SELECT * FROM trend_data ORDER BY timestamp DESC LIMIT 1";

    match client.query(stmt, &[]).await {
        Ok(v) => Ok(v),
        Err(e) => {
            bail!("error querying database: {e}");
        }
    }
}

async fn insert_trend_data(
    client: &Client,
    log_id: &i64,
    timestamp: &DateTime<Utc>,
    value: &Option<f64>,
) -> Result<Vec<Row>> {
    let stmt = "INSERT INTO trend_data (log_id, timestamp, value) VALUES ($1, $2, $3) RETURNING *;";

    match client.query(stmt, &[log_id, &timestamp, &value]).await {
        Ok(v) => Ok(v),
        Err(e) => {
            bail!("error querying database: {e}");
        }
    }
}

#[derive(Debug, Clone)]
pub struct MetadataTrendDataJoinDb {
    pub log_id: i64,
    pub source: String,
    pub unit: String,
    pub timestamp: DateTime<Utc>,
    pub value: Option<f64>,
}

#[derive(Debug, Clone)]
pub struct TrendDataDb {
    pub log_id: i64,
    pub timestamp: DateTime<Utc>,
    pub value: Option<f64>,
}

#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct MetadataDb {
    pub log_id: i64,
    pub source: String,
    pub unit: String,
    pub alias: Option<String>,
}

impl Serialize for TrendDataDb {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut s = serializer.serialize_struct("TrendDataDb", 3)?;
        s.serialize_field("log_id", &self.log_id)?;
        s.serialize_field("timestamp", &self.timestamp.to_rfc3339())?;
        s.serialize_field("value", &self.value)?;
        s.end()
    }
}

impl From<&Row> for MetadataTrendDataJoinDb {
    fn from(value: &Row) -> Self {
        let log_id: i64 = value.get("log_id");
        let source: String = value.get("source");
        let unit: String = value.get("unit");
        let timestamp: DateTime<Utc> = value.get("timestamp");
        let value: Option<f64> = value.get("value");

        MetadataTrendDataJoinDb {
            value,
            log_id,
            source,
            unit,
            timestamp,
        }
    }
}

impl From<&Row> for MetadataDb {
    fn from(value: &Row) -> MetadataDb {
        let log_id: i64 = value.get("log_id");
        let source: String = value.get("source");
        let unit: String = value.get("unit");
        let alias: Option<String> = value.get("unit");

        MetadataDb {
            log_id,
            source,
            unit,
            alias,
        }
    }
}

impl TryFrom<&TrendDataRaw> for TrendDataDb {
    type Error = anyhow::Error;

    fn try_from(value: &TrendDataRaw) -> core::result::Result<Self, Self::Error> {
        let naive_timestamp = NaiveDateTime::parse_from_str(&value.timestamp, "%Y-%m-%d %H:%M:%S");
        if let Err(e) = naive_timestamp {
            let timestamp = &value.timestamp;
            bail!("error parsing {timestamp}: {e}");
        }
        let mut naive_timestamp = naive_timestamp.unwrap();
        match value.timestamp_tzinfo {
            248 => {
                // CET
                naive_timestamp = naive_timestamp - TimeDelta::hours(1);
            }
            241 => {
                // CEST
                naive_timestamp = naive_timestamp - TimeDelta::hours(2);
            }
            _ => {
                let timezone = value.timestamp_tzinfo;
                bail!("did not recognize timestamp timezone {timezone}");
            }
        }

        let utc_time: DateTime<Utc> = naive_timestamp.and_local_timezone(Utc).unwrap();

        Ok(TrendDataDb {
            log_id: value.externallogid,
            timestamp: utc_time,
            value: value.value,
        })
    }
}

impl From<&Row> for TrendDataDb {
    fn from(row: &Row) -> TrendDataDb {
        let log_id: i64 = row.get("log_id");
        let timestamp: DateTime<Utc> = row.get("timestamp");
        let value: Option<f64> = row.get("value");

        TrendDataDb {
            log_id,
            timestamp,
            value,
        }
    }
}