rust-ef 1.7.0

Rust Entity Framework - An EFCore-inspired ORM for Rust
Documentation
//! HAVING expression AST: `HavingExpr`, `AggKind`, `CompareOp`.

use crate::provider::DbValue;

/// Aggregate function kind for `HavingExpr`.
///
/// Used by `linq!(having ...)` (Form B) when the having clause contains
/// nested boolean expressions (`AND`/`OR`/`NOT`) or aggregate-versus-aggregate
/// comparisons (`COUNT(b.id) > SUM(b.views)`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggKind {
    Count,
    Sum,
    Avg,
    Min,
    Max,
}

impl AggKind {
    /// Returns the SQL keyword for this aggregate.
    pub fn sql_name(&self) -> &'static str {
        match self {
            AggKind::Count => "COUNT",
            AggKind::Sum => "SUM",
            AggKind::Avg => "AVG",
            AggKind::Min => "MIN",
            AggKind::Max => "MAX",
        }
    }

    /// Parses an aggregate name (case-insensitive).
    pub fn from_name(name: &str) -> Option<Self> {
        match name.to_uppercase().as_str() {
            "COUNT" => Some(AggKind::Count),
            "SUM" => Some(AggKind::Sum),
            "AVG" => Some(AggKind::Avg),
            "MIN" => Some(AggKind::Min),
            "MAX" => Some(AggKind::Max),
            _ => None,
        }
    }
}

/// Comparison operator for `HavingExpr`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareOp {
    Eq,
    Ne,
    Gt,
    Ge,
    Lt,
    Le,
}

impl CompareOp {
    /// Returns the SQL operator string.
    pub fn sql_name(&self) -> &'static str {
        match self {
            CompareOp::Eq => "=",
            CompareOp::Ne => "!=",
            CompareOp::Gt => ">",
            CompareOp::Ge => ">=",
            CompareOp::Lt => "<",
            CompareOp::Le => "<=",
        }
    }

    /// Parses a comparison operator from its SQL symbol.
    pub fn from_symbol(symbol: &str) -> Option<Self> {
        match symbol {
            "=" => Some(CompareOp::Eq),
            "!=" => Some(CompareOp::Ne),
            ">" => Some(CompareOp::Gt),
            ">=" => Some(CompareOp::Ge),
            "<" => Some(CompareOp::Lt),
            "<=" => Some(CompareOp::Le),
            _ => None,
        }
    }
}

/// AST node for `HAVING` expressions.
///
/// Supports boolean combinations (`AND`/`OR`/`NOT`) and aggregate-versus-aggregate
/// comparisons in addition to the basic `agg(col) op value` form. Generated by
/// `linq!(having ...)` (Form B) expansion and compiled to SQL by `to_sql`.
#[derive(Debug, Clone)]
pub enum HavingExpr {
    /// `agg(col) op value` — basic comparison against a literal.
    Compare {
        agg: AggKind,
        col: String,
        op: CompareOp,
        value: DbValue,
    },
    /// `expr AND expr`.
    And(Box<HavingExpr>, Box<HavingExpr>),
    /// `expr OR expr`.
    Or(Box<HavingExpr>, Box<HavingExpr>),
    /// `NOT expr`.
    Not(Box<HavingExpr>),
    /// `agg(col1) op agg(col2)` — aggregate-vs-aggregate comparison (no bound parameter).
    CompareAgg {
        left_agg: AggKind,
        left_col: String,
        op: CompareOp,
        right_agg: AggKind,
        right_col: String,
    },
}

impl HavingExpr {
    /// Recursively compiles the expression into a SQL fragment using the
    /// provider-specific placeholder syntax (`?` for SQLite/MySQL, `$N` for
    /// PostgreSQL).
    ///
    /// `param_idx` is advanced past each bound parameter in left-to-right
    /// order, matching the order produced by [`HavingExpr::collect_params`].
    /// This ensures PostgreSQL's 1-indexed `$N` placeholders stay contiguous
    /// with the WHERE clause's placeholders.
    pub fn to_sql(
        &self,
        gen: &dyn crate::provider::ISqlGenerator,
        param_idx: &mut usize,
    ) -> String {
        match self {
            Self::Compare {
                agg,
                col,
                op,
                value: _,
            } => {
                let placeholder = gen.parameter_placeholder(*param_idx);
                *param_idx += 1;
                format!(
                    "{}({}) {} {}",
                    agg.sql_name(),
                    col,
                    op.sql_name(),
                    placeholder
                )
            }
            Self::And(left, right) => format!(
                "({} AND {})",
                left.to_sql(gen, param_idx),
                right.to_sql(gen, param_idx)
            ),
            Self::Or(left, right) => format!(
                "({} OR {})",
                left.to_sql(gen, param_idx),
                right.to_sql(gen, param_idx)
            ),
            Self::Not(inner) => format!("NOT ({})", inner.to_sql(gen, param_idx)),
            Self::CompareAgg {
                left_agg,
                left_col,
                op,
                right_agg,
                right_col,
            } => format!(
                "{}({}) {} {}({})",
                left_agg.sql_name(),
                left_col,
                op.sql_name(),
                right_agg.sql_name(),
                right_col
            ),
        }
    }

    /// Collects bound parameter values in the same left-to-right order that
    /// [`HavingExpr::to_sql`] emits placeholders. Used to populate the query
    /// parameter vector at registration time so that `compile_sql` returns
    /// params matching the placeholder order.
    pub fn collect_params(&self) -> Vec<DbValue> {
        match self {
            Self::Compare { value, .. } => vec![value.clone()],
            Self::And(left, right) | Self::Or(left, right) => {
                let mut v = left.collect_params();
                v.extend(right.collect_params());
                v
            }
            Self::Not(inner) => inner.collect_params(),
            Self::CompareAgg { .. } => Vec::new(),
        }
    }
}