qraft-core 0.1.2

Core type system, query model, decoding, and SQL lowering primitives for qraft.
Documentation
//! Delete statement builders.

use crate::{
    HasDialect, ModelQueryPolicy, Qrafting, Query, QueryOf,
    builder::Update,
    cte::IntoCtes,
    emitter::Emitter,
    query::{LowerFilter, Table, TypedCompiled, rewrite_params},
};

type SoftDeleteFn<T> = fn(Query) -> Option<Update<T>>;

/// A typed delete statement under construction.
pub struct Delete<T> {
    /// Delete target table.
    pub table: Table<T>,
    /// Shared query state used for filters, CTEs, and parameters.
    pub query: Query,
    soft_delete: Option<SoftDeleteFn<T>>,
}

/// Starts a delete statement for the given table.
///
/// # Examples
///
/// ```rust
/// use qraft_core::{BigInt, Sqlite, builder::delete::delete_from, expression::col, query::Table};
///
/// let users = Table::<()>::new("users");
/// let sql = delete_from(users)
///     .filter(col::<BigInt>("id").eq(3_i64))
///     .to_debug_sql::<Sqlite>();
///
/// assert_eq!(sql, r#"delete from "users" where "id" = ?; params=[3]"#);
/// ```
pub fn delete_from<T>(table: Table<T>) -> Delete<T> {
    Delete {
        table,
        query: Query::from(table),
        soft_delete: None,
    }
}

impl<T> Delete<T> {
    /// Attaches a common table expression to the delete.
    pub fn with<C>(mut self, ctes: C) -> Self
    where
        C: IntoCtes,
    {
        self.query = self.query.with(ctes);
        self
    }

    /// Attaches a recursive common table expression to the delete.
    pub fn with_recursive<C>(mut self, ctes: C) -> Self
    where
        C: IntoCtes,
    {
        self.query = self.query.with_recursive(ctes);
        self
    }

    /// Adds a `where` predicate combined with `and`.
    pub fn filter<F>(mut self, clause: F) -> Self
    where
        F: LowerFilter,
    {
        self.query = self.query.filter(clause);
        self
    }

    /// Adds a `where` predicate combined with `or`.
    pub fn or_filter<F>(mut self, clause: F) -> Self
    where
        F: LowerFilter,
    {
        self.query = self.query.or_filter(clause);
        self
    }

    /// Compiles the delete into the typed representation used by executors.
    pub fn into_compiled<D: HasDialect>(mut self) -> TypedCompiled<T> {
        if let Some(soft_delete) = self.soft_delete
            && let Some(update) = soft_delete(self.query.clone())
        {
            return update.into_compiled::<D>();
        }

        let sql = self.to_sql::<D>();

        TypedCompiled {
            sql,
            params: self.query.params,
            data: self.query.data,
            marker: std::marker::PhantomData,
        }
    }

    /// Emits SQL with appended debug parameter output.
    pub fn to_debug_sql<D: HasDialect>(&mut self) -> String {
        if let Some(soft_delete) = self.soft_delete
            && let Some(mut update) = soft_delete(self.query.clone())
        {
            return update.to_debug_sql::<D>();
        }

        let mut sql = self.to_sql::<D>();
        self.query.debug_params(&mut sql).unwrap();
        sql
    }

    /// Emits SQL for the requested dialect.
    pub fn to_sql<D: HasDialect>(&mut self) -> String {
        if let Some(soft_delete) = self.soft_delete
            && let Some(mut update) = soft_delete(self.query.clone())
        {
            return update.to_sql::<D>();
        }

        let mut writer = String::new();
        let mut directives = Vec::new();
        let mut indexes = Vec::new();

        let mut emitter = Emitter::new(
            &mut writer,
            &self.query.data,
            D::DIALECT,
            &mut directives,
            &mut indexes,
        );

        emitter.emit_delete(self).unwrap();

        rewrite_params(&indexes, &mut self.query.params);

        writer
    }
}

impl<M> QueryOf<M>
where
    M: Qrafting,
{
    pub fn delete_query(self) -> Delete<M> {
        let table = Table::new(M::TABLE);
        Delete {
            table,
            query: self.into(),
            soft_delete: Some(<M::QueryPolicy as ModelQueryPolicy<M>>::soft_delete),
        }
    }

    pub async fn delete<E>(self, exec: E) -> quex::Result<quex::ExecResult>
    where
        E: quex::Executor,
    {
        self.delete_query().execute(exec).await
    }

    pub fn force_delete_query(self) -> Delete<M> {
        let table = Table::new(M::TABLE);
        Delete {
            table,
            query: self.into(),
            soft_delete: None,
        }
    }

    pub async fn force_delete<E>(self, exec: E) -> quex::Result<quex::ExecResult>
    where
        E: quex::Executor,
    {
        self.force_delete_query().execute(exec).await
    }

    pub fn restore_query(self) -> Update<M> {
        <M::QueryPolicy as ModelQueryPolicy<M>>::restore(self.into())
            .expect("QueryOf::restore_query() is only available for soft-deletable qraft models")
    }

    pub async fn restore<E>(self, exec: E) -> quex::Result<quex::ExecResult>
    where
        E: quex::Executor,
    {
        self.restore_query().execute(exec).await
    }
}

impl<T> Delete<T> {
    pub fn force(mut self) -> Self {
        self.soft_delete = None;
        self
    }
}

impl<T> Delete<T> {
    fn from_query(table: Table<T>, query: Query) -> Self {
        Self {
            table,
            query,
            soft_delete: None,
        }
    }
}

impl Query {
    pub fn delete_query(self) -> Delete<()> {
        let table = self
            .base_table_static()
            .expect("Query::delete() requires a base table created from a static table source");
        Delete::from_query(Table::new(table), self)
    }

    pub async fn delete<E>(self, exec: E) -> quex::Result<quex::ExecResult>
    where
        E: quex::Executor,
    {
        self.delete_query().execute(exec).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        Sqlite,
        expression::{Col, PredicateExt},
        tests::{User, id, table},
    };

    #[test]
    fn test_simple_delete_stmt() {
        let stmt = delete_from(table).to_debug_sql::<Sqlite>();
        assert_eq!(stmt, r#"delete from "users"; params=[]"#);
    }

    #[test]
    fn test_filter_delete_stmt() {
        let stmt = delete_from(table)
            .filter("id".bigint().eq(10))
            .to_debug_sql::<Sqlite>();
        assert_eq!(stmt, r#"delete from "users" where "id" = ?; params=[10]"#);
    }

    #[test]
    fn test_filter_complex_delete_stmt() {
        let stmt = delete_from(table)
            .filter("id".bigint().eq(10).or("name".bigint().eq(1)))
            .filter("username".bigint().eq(10))
            .to_debug_sql::<Sqlite>();
        assert_eq!(
            stmt,
            r#"delete from "users" where ("id" = ? or "name" = ?) and "username" = ?; params=[10, 1, 10]"#
        );
    }

    #[test]
    fn test_delete_query() {
        let stmt = Query::from("users")
            .filter(id.eq(1))
            .typed::<User>()
            .delete_query()
            .to_debug_sql::<Sqlite>();

        assert_eq!(
            stmt,
            r#"delete from "users" where "users"."id" = ?; params=[1]"#
        );
    }

    #[test]
    fn test_delete_or_filter_combines_with_existing_where_clause() {
        let stmt = delete_from(table)
            .filter(id.eq(1))
            .or_filter(id.eq(2))
            .to_debug_sql::<Sqlite>();

        assert_eq!(
            stmt,
            r#"delete from "users" where "users"."id" = ? or "users"."id" = ?; params=[1, 2]"#
        );
    }
}