modio-logger-db 0.11.1

modio-logger Dbus service
Documentation
// Author: D.S. Ljungmark <spider@skuggor.se>, Modio AB
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::error::Error;
use sqlx::migrate::Migrator;
pub use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
use sqlx::sqlite::{SqlitePoolOptions, SqliteSynchronous};
use sqlx::ConnectOptions;
use std::time::Duration;
use tracing::{error, info, warn};

// Embed our database migrations
static MIGRATOR: Migrator = sqlx::migrate!();

async fn get_file_pool(
    filename: &std::path::Path,
    db_connections: u32,
    timeout: Duration,
) -> Result<SqlitePool, Error> {
    // Maybe we need to use JournalMode WAL to avoid locked tables,
    // but we would prefer not to on the firmware.
    //    use sqlx::sqlite::SqliteJournalMode;
    //    and conn.journal_mode(SqliteJournalMode::WAL)

    // Default slow query log is 1s, if our acquire_timeout is 2s and we do 2 slow queries, we
    // won't get a warning about what is slow.
    // This sets it to 0.3s
    let slow_log = Duration::from_secs_f32(0.3);

    info!(
        "Opening SQLite database file: {}, connections={}, timeout={}",
        filename.display(),
        db_connections,
        timeout.as_secs_f32()
    );
    let conn = SqliteConnectOptions::new()
        .filename(filename)
        .synchronous(SqliteSynchronous::Normal)
        .shared_cache(false)
        .pragma("temp_store", "memory")
        .log_statements(log::LevelFilter::Trace)
        .log_slow_statements(log::LevelFilter::Warn, slow_log);

    let pool = SqlitePoolOptions::new()
        .min_connections(db_connections)
        .max_connections(db_connections)
        .test_before_acquire(false)
        .acquire_timeout(timeout)
        .connect_with(conn)
        .await?;
    Ok(pool)
}

pub async fn run_migrations(pool: &SqlitePool) -> Result<(), Error> {
    MIGRATOR.run(pool).await?;
    Ok(())
}

/// Helper around building a pool
pub struct SqlitePoolBuilder<'tempfile> {
    path: Option<&'tempfile std::path::Path>,
    migrate: bool,
    db_connections: u32,
    timeout: Duration,
}

impl<'tempfile> SqlitePoolBuilder<'tempfile> {
    #[must_use]
    pub const fn new() -> Self {
        Self {
            path: None,
            migrate: true,
            db_connections: 4,
            // The since DBus has a timeout of ~25s by default, we should have a timeout that's at least
            // _lower_ than that so we can return errors before things go totally wrong.
            timeout: Duration::from_secs(5),
        }
    }

    /// Set the `db_path`
    /// It is expected that it is a reference to a path that should exist on disk,
    /// and be visible by it's filename.
    ///
    /// The caller is expected to ensure that the file exists.
    #[must_use]
    pub const fn db_path(mut self, path: &'tempfile std::path::Path) -> Self {
        self.path = Some(path);
        self
    }
    /// Set the amount of db_connections to use
    /// Use this to test if the connection count matters for SQLite on devices.
    #[must_use]
    pub const fn db_connections(mut self, db_connections: Option<u32>) -> Self {
        if let Some(conns) = db_connections {
            self.db_connections = conns;
        }
        self
    }
    #[must_use]
    pub const fn db_timeout(mut self, db_timeout: Option<u32>) -> Self {
        if let Some(timeout) = db_timeout {
            assert!(
                timeout <= 25,
                "DBus timeout is ~25s. timeout should be less."
            );
            self.timeout = Duration::from_secs(timeout as u64);
        }
        self
    }
    #[must_use]
    pub const fn migrate(mut self, migrate: bool) -> Self {
        self.migrate = migrate;
        self
    }

    pub async fn build(self) -> Result<SqlitePool, Error> {
        let db_path = self.path.expect("Must have a path");
        if self.db_connections < 2 {
            error!(
                "Too few connections to function. conns = {} < 2",
                self.db_connections
            );
        }
        let pool = get_file_pool(db_path, self.db_connections, self.timeout).await?;

        if self.migrate {
            warn!("Running migrations on {:?}", &db_path);
            run_migrations(&pool).await?;
        }
        Ok(pool)
    }
}

impl Default for SqlitePoolBuilder<'_> {
    fn default() -> Self {
        Self::new()
    }
}