luncheon 0.2.11

A library to make working with SQLITE in a more typed and traited way
Documentation
use std::marker::PhantomData;

use rusqlite::{params, Row};
use thiserror::Error;

#[cfg(feature="tracing")]
use tracing::debug;

use crate::{Database, Res};

pub trait Table {
    const TBL_NAME: &str;
    const CREATE_QUERY: &str;

    const TABLE_SCHEMA_QUERY: &str = "SELECT sql FROM sqlite_schema WHERE tbl_name=?1 AND type='table' AND name NOT LIKE 'sqlite_%'";

    fn tbl_name() -> &'static str {
        Self::TBL_NAME
    }

    fn create_table(database: &Database) -> Res<bool> {
        if Self::CREATE_QUERY
            .starts_with(format!("CREATE TABLE IF NOT EXISTS {}", Self::TBL_NAME).as_str())
        {
            debug!("Skip on not exists");
        } else if Self::CREATE_QUERY
            .starts_with(format!("CREATE TABLE {}", Self::TBL_NAME).as_str())
        {
            debug!("Fail on Exists");
        } else {
            return Err(format!(
                "'{}' is not a valid CREATE TABLE command",
                Self::CREATE_QUERY
            )
            .into());
        };
        database.execute(Self::CREATE_QUERY)?;
        Ok(true)
    }

    /// Checks if the table exists in the database
    fn exists_db(database: &Database) -> Res<bool> {
        let func = |row: &Row| -> Result<String, rusqlite::Error> { row.get(0) };

        let mut stmt = database.prepare_reader(Self::TABLE_SCHEMA_QUERY)?;
        let rows: Box<[String]> = stmt
            .query_map(params![Self::TBL_NAME], func)?
            .flatten()
            .collect();
        if rows.len() == 1 {
            Ok(true)
        } else if rows.is_empty() {
            Ok(false)
        } else {
            let path = database.path_str();
            Err(format!(
                "There are {} tables with {} table_name at the db at {}",
                rows.len(),
                Self::TBL_NAME,
                path
            )
            .into())
        }
    }

    fn is_initialized(database: &Database) -> Res<()> {
        let mut stmt = database.prepare_reader(Self::TABLE_SCHEMA_QUERY)?;
        let current_schema = stmt.query_row(
            params![Self::TBL_NAME],
            |row| -> Result<String, rusqlite::Error> { row.get(0) },
        )?;
        if current_schema == Self::CREATE_QUERY {
            Ok(())
        } else {
            Err(format!(
                "Expected {} schema, got {} schema for {} table",
                Self::CREATE_QUERY,
                current_schema,
                Self::TBL_NAME
            )
            .into())
        }
    }

    fn len(database: &Database) -> Res<usize> {
        let func = |row: &Row| -> Result<usize, rusqlite::Error> { row.get(1) };
        let mut stmt = database.prepare_reader("SELECT COUNT(*) FROM ?1")?;
        let len = stmt.query_row(params![Self::TBL_NAME], func)?;
        Ok(len)
    }

    fn is_empty(database: &Database) -> Res<bool> {
        Ok(Self::len(database)? > 1)
    }

    /// I've had weird bugs with 'SELECT *' before
    fn select_asterisk<R, F: Fn(&Row) -> Result<R, rusqlite::Error>>(
        database: &Database,
        func: F,
    ) -> Res<Vec<R>> {
        let mut stmt = database.prepare_reader("SELECT * FROM ?1")?;
        let rows = stmt
            .query_map(params![Self::TBL_NAME], func)?
            .flatten()
            .collect();
        Ok(rows)
    }
}

/// A handler that can only be created if the table exists and matches the stored schema
pub struct OwnedTable<T: Table>(PhantomData<T>);

impl<T: Table> OwnedTable<T> {
    pub fn create(database: &Database) -> Res<Self> {
        if !T::exists_db(database)? {
            T::create_table(database)?;
        };

        T::is_initialized(database)?;

        Ok(Self(PhantomData))
    }
}

#[derive(Error, Debug)]
pub enum OwnedTableError {
    #[error("Old Schema:\n{0}\nNew Schema:\n{1}")]
    MismatchedSchema(String, String),
}