floz-orm 0.1.7

A lightweight, typesafe Rust ORM — unifying DAO and DSL from a single schema
Documentation
//! DELETE query builder.
//!
//! ```ignore
//! let (sql, params) = DeleteQuery::new("users")
//!     .where_(col_id.eq(1))
//!     .to_sql()?;
//! // "DELETE FROM users WHERE id = $1"
//! ```

use std::fmt::Write;

use crate::error::FlozError;
use crate::expr::Expr;
use crate::value::Value;
use super::quote_table;

/// A DELETE query builder.
///
/// Safety: `to_sql()` returns `FlozError::UnguardedDelete` if neither `.where_()` nor
/// `.all()` is called, to prevent accidental table-wide deletes.
#[derive(Debug, Clone)]
pub struct DeleteQuery {
    table: String,
    where_clause: Option<Expr>,
    all: bool,
    returning: ReturningClause,
}

#[derive(Debug, Clone)]
enum ReturningClause {
    None,
    All,
    Cols(Vec<String>),
}

impl DeleteQuery {
    /// Create a new DELETE query for the given table.
    pub fn new(table: &str) -> Self {
        Self {
            table: table.to_string(),
            where_clause: None,
            all: false,
            returning: ReturningClause::None,
        }
    }

    /// Add a WHERE clause.
    pub fn where_(mut self, expr: Expr) -> Self {
        self.where_clause = Some(expr);
        self
    }

    /// Explicitly permit deleting all rows (without WHERE).
    /// This is a safety mechanism — you must explicitly opt in.
    pub fn all(mut self) -> Self {
        self.all = true;
        self
    }

    /// Add RETURNING *.
    pub fn returning_all(mut self) -> Self {
        self.returning = ReturningClause::All;
        self
    }

    /// Add RETURNING for specific columns.
    pub fn returning_cols(mut self, columns: &[&str]) -> Self {
        self.returning = ReturningClause::Cols(
            columns.iter().map(|c| c.to_string()).collect(),
        );
        self
    }

    /// Render to SQL string + parameters.
    ///
    /// Returns `FlozError::UnguardedDelete` if neither `.where_()` nor `.all()` is set.
    pub fn to_sql(&self) -> Result<(String, Vec<Value>), FlozError> {
        if self.where_clause.is_none() && !self.all {
            return Err(FlozError::UnguardedDelete);
        }

        let mut sql = String::with_capacity(64);
        let mut params = Vec::new();
        let mut idx = 0usize;

        // DELETE FROM table
        write!(sql, "DELETE FROM {}", quote_table(&self.table)).unwrap();

        // WHERE
        if let Some(ref expr) = self.where_clause {
            sql.push_str(" WHERE ");
            expr.to_sql(&mut sql, &mut params, &mut idx);
        }

        // RETURNING
        match &self.returning {
            ReturningClause::None => {}
            ReturningClause::All => sql.push_str(" RETURNING *"),
            ReturningClause::Cols(cols) => {
                sql.push_str(" RETURNING ");
                for (i, col) in cols.iter().enumerate() {
                    if i > 0 { sql.push_str(", "); }
                    sql.push_str(col);
                }
            }
        }

        Ok((sql, params))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::column::Column;

    fn col_id() -> Column<i32> { Column::new("id", "users") }
    fn col_age() -> Column<i32> { Column::new("age", "users") }

    #[test]
    fn delete_with_where() {
        let (sql, params) = DeleteQuery::new("users")
            .where_(col_id().eq(1))
            .to_sql()
            .unwrap();
        assert_eq!(sql, "DELETE FROM users WHERE id = $1");
        assert_eq!(params, vec![Value::Int(1)]);
    }

    #[test]
    fn delete_complex_where() {
        let (sql, params) = DeleteQuery::new("users")
            .where_(col_age().lt(18).or(col_age().gt(100)))
            .to_sql()
            .unwrap();
        assert_eq!(sql, "DELETE FROM users WHERE (age < $1 OR age > $2)");
        assert_eq!(params.len(), 2);
    }

    #[test]
    fn delete_all() {
        let (sql, params) = DeleteQuery::new("users")
            .all()
            .to_sql()
            .unwrap();
        assert_eq!(sql, "DELETE FROM users");
        assert!(params.is_empty());
    }

    #[test]
    fn delete_unguarded_error() {
        let result = DeleteQuery::new("users").to_sql();
        assert!(matches!(result, Err(FlozError::UnguardedDelete)));
    }

    #[test]
    fn delete_returning() {
        let (sql, _) = DeleteQuery::new("users")
            .where_(col_id().eq(1))
            .returning_all()
            .to_sql()
            .unwrap();
        assert!(sql.ends_with("RETURNING *"));
    }

    #[test]
    fn delete_returning_cols() {
        let (sql, _) = DeleteQuery::new("users")
            .where_(col_id().eq(1))
            .returning_cols(&["id", "name"])
            .to_sql()
            .unwrap();
        assert!(sql.ends_with("RETURNING id, name"));
    }

    #[test]
    fn delete_schema_qualified() {
        let (sql, _) = DeleteQuery::new("auth.users")
            .all()
            .to_sql()
            .unwrap();
        assert_eq!(sql, r#"DELETE FROM "auth"."users""#);
    }
}