wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
use crate::prelude::*;
use r2d2::PooledConnection;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::Row;

/// The `LIMIT`/`OFFSET` tail of a listing query, with the values to bind.
///
/// Every repository's `list_impl` takes an offset and a limit, and every one of
/// them used to drop both on the floor and return the whole table — so `list`
/// silently meant `list everything` and a paged caller got the same answer for
/// every page. Building the clause in one place is what keeps that from being
/// five independent things to remember.
///
/// SQLite has no bare `OFFSET`; it is only legal after a `LIMIT`. An offset with
/// no limit therefore becomes `LIMIT -1` — every row, starting at the offset.
/// A negative offset or limit is refused rather than passed to SQLite, which
/// would take it as "no limit" and quietly return a different page than asked
/// for.
pub fn page_clause(offset: Option<i64>, limit: Option<i64>) -> Result<(String, Vec<i64>)> {
    if let Some(value) = offset {
        if value < 0 {
            bail!("offset must not be negative, got {value}");
        }
    }
    if let Some(value) = limit {
        if value < 0 {
            bail!("limit must not be negative, got {value}");
        }
    }

    Ok(match (offset, limit) {
        (None, None) => (String::new(), Vec::new()),
        (None, Some(limit)) => (" LIMIT ?".to_string(), vec![limit]),
        (Some(offset), None) => (" LIMIT -1 OFFSET ?".to_string(), vec![offset]),
        (Some(offset), Some(limit)) => (" LIMIT ? OFFSET ?".to_string(), vec![limit, offset]),
    })
}

// Every method here is `async` only to fit callers that are; each body is the
// synchronous `*_impl` beneath it. Desugaring to `impl Future + Send` would
// state a bound the futures already satisfy and change the trait's shape for
// every implementor, so the lint is answered rather than obeyed.
#[allow(async_fn_in_trait)]
pub trait Repository<T: Clone> {
    fn get_connection(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>>;
    fn map_row(row: &Row) -> Result<T>;

    fn create_impl(
        &self,
        entity: &T,
        conn: &PooledConnection<SqliteConnectionManager>,
    ) -> Result<()>;
    fn find_by_id_impl(
        &self,
        id: &str,
        conn: &PooledConnection<SqliteConnectionManager>,
    ) -> Result<Option<T>>;
    fn update_impl(
        &self,
        entity: &T,
        conn: &PooledConnection<SqliteConnectionManager>,
    ) -> Result<()>;
    fn upsert_impl(
        &self,
        entity: &T,
        conn: &PooledConnection<SqliteConnectionManager>,
    ) -> Result<()>;
    fn delete_impl(&self, id: &str, conn: &PooledConnection<SqliteConnectionManager>)
        -> Result<()>;
    fn list_impl(
        &self,
        conn: &PooledConnection<SqliteConnectionManager>,
        offset: Option<i64>,
        limit: Option<i64>,
    ) -> Result<Vec<T>>;
    fn count_impl(&self, conn: &PooledConnection<SqliteConnectionManager>) -> Result<i64>;

    async fn create(&self, entity: &T) -> Result<()> {
        let conn = self.get_connection()?;
        self.create_impl(entity, &conn)
    }

    async fn find_by_id(&self, id: &str) -> Result<Option<T>> {
        let conn = self.get_connection()?;
        self.find_by_id_impl(id, &conn)
    }

    async fn update(&self, entity: &T) -> Result<()> {
        let conn = self.get_connection()?;
        self.update_impl(entity, &conn)
    }

    async fn upsert(&self, entity: &T) -> Result<()> {
        let conn = self.get_connection()?;
        self.upsert_impl(entity, &conn)
    }

    async fn delete(&self, id: &str) -> Result<()> {
        let conn = self.get_connection()?;
        self.delete_impl(id, &conn)
    }

    async fn list(&self, offset: Option<i64>, limit: Option<i64>) -> Result<Vec<T>> {
        let conn = self.get_connection()?;
        self.list_impl(&conn, offset, limit)
    }

    async fn count(&self) -> Result<i64> {
        let conn = self.get_connection()?;
        self.count_impl(&conn)
    }
}