luncheon 0.2.11

A library to make working with SQLITE in a more typed and traited way
Documentation
use delete::Delete;
use insert::Insert;
use rusqlite::Statement;
use select::Select;
use table::Table;

use std::path::Path;
use thiserror::Error;
use tracing::{debug, info};
use update::Update;

use rusqlite::{Connection, ToSql, params};

use crate::upsert::Upsert;

pub mod delete;
pub mod insert;
pub mod macros;
pub mod search;
pub mod select;
pub mod settings;
pub mod table;
pub mod trigger;
pub mod update;
pub mod upsert;

#[cfg(feature = "alpha")]
pub mod migration;
#[cfg(feature = "alpha")]
pub mod tracing_table;

#[derive(Error, Debug)]
pub enum Errors {
    #[error("{0}")]
    Rusqlite(#[from] rusqlite::Error),
    #[error("{0}")]
    StdIo(#[from] std::io::Error),
    #[error("{0}")]
    Custom(String),
}

impl From<String> for Errors {
    fn from(value: String) -> Self {
        Self::Custom(value)
    }
}

impl From<&str> for Errors {
    fn from(value: &str) -> Self {
        Self::Custom(value.to_string())
    }
}

pub type Res<T> = Result<T, Errors>;

pub struct Database {
    reader_conn: Connection,
    writer_conn: Connection,
}

fn set_pragmas(conn: Connection) -> Res<Connection> {
    conn.query_row("PRAGMA journal_mode = WAL;", (), |_row| Ok(()))?;
    conn.query_row("PRAGMA journal_size_limit = 614400;", (), |_row| Ok(()))?;
    conn.query_row("PRAGMA busy_timeout = 3000;", (), |_| Ok(()))?;

    conn.execute("PRAGMA page_size = 32768;", ())?;
    conn.execute("PRAGMA foreign_keys = ON;", ())?;
    conn.execute("PRAGMA synchronous = NORMAL;", ())?;
    Ok(conn)
}

impl Database {
    pub fn open<P: AsRef<Path>>(p: P) -> Res<Self> {
        let reader_conn = Connection::open(p.as_ref())?;
        let reader_conn = set_pragmas(reader_conn)?;
        let writer_conn = Connection::open(p.as_ref())?;
        let writer_conn = set_pragmas(writer_conn)?;
        Ok(Self {
            reader_conn,
            writer_conn,
        })
    }

    pub fn reader_conn(&self) -> &Connection {
        &self.reader_conn
    }

    pub fn writer_conn(&self) -> &Connection {
        &self.writer_conn
    }

    /// Hides the return  
    pub fn query_empty(&self, query: &str) -> Res<()> {
        #[cfg(feature = "tracing")]
        debug!("Empty Query: {query}");

        self.writer_conn.query_row(query, (), |row| {
            #[cfg(feature = "tracing")]
            debug!("Result from empty query: {row:#?}");

            Ok(())
        })?;
        Ok(())
    }

    pub fn path_str(&self) -> &str {
        self.reader_conn.path().unwrap_or(":memory:")
    }

    pub fn vacuum(&self) -> Res<()> {
        info!("Starting vacuum");
        self.writer_conn.execute("PRAGMA vacuum;", ())?;
        Ok(())
    }

    pub fn tables(&self) -> Res<Vec<String>> {
        let mut stmt = self.reader_conn.prepare(
            "SELECT name FROM pragma_table_list WHERE type='table' AND name NOT LIKE 'sqlite_%'",
        )?;
        let out = stmt
            .query_map((), |row| {
                let s = row.get(0)?;
                Ok(s)
            })?
            .flatten()
            .collect();
        Ok(out)
    }

    pub fn get_table<S: AsRef<str>>(&self, table_name: S) -> Res<Vec<(String, String)>> {
        let mut stmt = self
            .reader_conn
            .prepare("SELECT name,type FROM pragma_table_info(?1)")?;
        let rows = stmt
            .query_map(params![table_name.as_ref()], |row| {
                let n = row.get(0)?;
                let t = row.get(1)?;
                Ok((n, t))
            })?
            .flatten()
            .collect();
        Ok(rows)
    }

    pub fn execute<S: AsRef<str>>(&self, query: S) -> Res<()> {
        self.writer_conn.execute(query.as_ref(), ())?;
        Ok(())
    }

    pub fn prepare_reader<S: AsRef<str>>(&self, query: S) -> Res<Statement<'_>> {
        let stmt = self.reader_conn.prepare(query.as_ref())?;
        Ok(stmt)
    }

    pub fn prepare_writer<S: AsRef<str>>(&self, query: S) -> Res<Statement<'_>> {
        let stmt = self.writer_conn.prepare(query.as_ref())?;
        Ok(stmt)
    }
}

/// Helpers for the various query traits
impl Database {
    pub fn insert<T: Table, R: Insert<T>>(&self, item: R) -> Res<()> {
        R::insert(self, item)
    }

    pub fn upsert<T: Table, R: Upsert<T>>(&self, item: R) -> Res<()> {
        R::upsert(self, item)
    }

    pub fn create_table<C: Table>(&self) -> Res<bool> {
        C::create_table(self)
    }
    pub fn select<R, S: Select<R>>(&self) -> Res<Vec<R>> {
        S::select(self)
    }
    pub fn update<I: PartialEq + ToSql, E: ToSql, U: Update<I, E>>(
        &self,
        id: I,
        new_value: E,
    ) -> Res<()> {
        U::update(self, id, new_value)
    }
    pub fn delete<I: PartialEq + ToSql, D: Delete<I>>(&self, id: I) -> Res<()> {
        D::delete(self, id)
    }
}

/// Traces the errors when flattening
pub fn flatten_traced<R, I: Iterator<Item = Result<R, impl std::error::Error>>>(
    i: I,
) -> impl Iterator<Item = R> {
    i.filter_map(|x| {
        if x.is_ok() {
            Some(x.unwrap())
        } else {
            #[cfg(feature = "tracing")]
            tracing::error!("Error when flattening: {}", x.err().unwrap());
            None
        }
    })
}

// #[cfg(feature = "alpha")]
// /// Currently for testing sending tracing data to a sqlite dbs "tracing" table
// /// Would add lot of writes, but this lib is focused on low-write use-cases
// impl Write for Database {
//     fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
//         todo!()
//     }

//     fn flush(&mut self) -> std::io::Result<()> {
//         {
//             let mut stmt = self
//             .prepare_writer(
//                 "INSERT INTO tracing(name, target, level, fields, file_name, line_number, module_path) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
//             )
//             .map_err(|_e| std::io::Error::from_raw_os_error(1))?;

//             for span in self.tracing_buf.iter() {
//                 if let Some(m) = span.metadata() {
//                     let level = m.level().to_string();
//                     let fields = m.fields().to_string();
//                     stmt.execute(params![
//                         m.name(),
//                         m.target(),
//                         level,
//                         fields,
//                         m.file(),
//                         m.line(),
//                         m.module_path()
//                     ]);
//                 } else {
//                     continue;
//                 };
//             }
//         }
//         self.writer_conn.cache_flush().expect("Error cache flush");
//         self.tracing_buf.clear();
//         Ok(())
//     }
// }

// pub enum SqliteType {
//     Text,
//     Integer,
//     Blob,
//     Real,
//     Boolean,
//     Any,
// }

// impl Display for SqliteType {
//     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
//         let strng = match self {
//             SqliteType::Text => "TEXT",
//             SqliteType::Integer => "INTEGER",
//             SqliteType::Blob => "BLOB",
//             SqliteType::Real => "REAL",
//             SqliteType::Boolean => "BOOLEAN",
//             SqliteType::Any => "ANY",
//         };
//         write!(f, "{strng}")
//     }
// }

// impl TryFrom<&str> for SqliteType {
//     type Error = Errors;

//     fn try_from(value: &str) -> Result<Self, Self::Error> {
//         let lower = value.to_ascii_lowercase();
//         let out = match lower.as_str() {
//             "int" | "integer" => Self::Integer,
//             "text" | "varchar" => Self::Text,
//             "real" | "float" => Self::Real,
//             "blob" => Self::Blob,
//             "bool" | "boolean" => Self::Boolean,
//             "any" | "" => Self::Any,
//             _ => return Err(format!("'{lower}' is not a sqlite type").into()),
//         };
//         Ok(out)
//     }
// }

// impl FromSql for SqliteType {
//     fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
//         match value {
//             ValueRef::Null => todo!(),
//             ValueRef::Integer(_) => todo!(),
//             ValueRef::Real(_) => todo!(),
//             ValueRef::Text(items) => {
//                 let t = from_utf8(items).expect("1");
//                 FromSqlResult::Ok(SqliteType::try_from(t).expect("2"))
//             }
//             ValueRef::Blob(items) => todo!(),
//         }
//     }
// }

// impl ToSql for SqliteType {
//     fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
//         todo!()
//     }
// }

/// Generic query trait
/// For more uncommon queries
pub trait Query<T: Table> {
    const QUERY: &str;
    fn stmt(database: &Database) -> Res<Statement<'_>> {
        database.prepare_writer(Self::QUERY)
    }
}