rustango 0.14.2

Django-shaped web framework for Rust: ORM, migrations, auto-admin, multi-tenancy, audit log.
Documentation
//! Typed column references — the compile-time-checked side of the query API.
//!
//! `#[derive(Model)]` emits a zero-sized type per scalar field, attaches a
//! `Column` impl, and re-exports it as an inherent `pub const` on the
//! struct. Users get `User::id`, `User::name`, etc., each carrying its own
//! `Value` type, so `User::id.eq("alice")` is a *compile* error rather than
//! a runtime `TypeMismatch`.

use std::marker::PhantomData;

use super::{Assignment, Filter, Model, Op, SqlValue, WhereExpr};

/// A typed reference to a single scalar column on `Self::Model`.
///
/// Implementations are generated by `#[derive(Model)]`; users normally
/// reach them through `User::<field>` consts, not by naming the trait.
pub trait Column: Copy + 'static {
    /// The model the column belongs to.
    type Model: Model;
    /// The Rust-side type of the column. Must be convertible into `SqlValue`.
    type Value: Into<SqlValue>;
    /// Rust-side field name.
    const NAME: &'static str;
    /// SQL-side column name.
    const COLUMN: &'static str;
    /// Dialect-neutral classification.
    const FIELD_TYPE: super::FieldType;

    /// `column = value`.
    fn eq<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::Eq, value.into().into())
    }

    /// `column <> value`.
    fn ne<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::Ne, value.into().into())
    }

    /// `column < value`.
    fn lt<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::Lt, value.into().into())
    }

    /// `column <= value`.
    fn lte<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::Lte, value.into().into())
    }

    /// `column > value`.
    fn gt<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::Gt, value.into().into())
    }

    /// `column >= value`.
    fn gte<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::Gte, value.into().into())
    }

    /// `column LIKE value` — case-sensitive on Postgres.
    fn like<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::Like, value.into().into())
    }

    /// `column IS NULL`.
    fn is_null(self) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::IsNull, SqlValue::Bool(true))
    }

    /// `column IS NOT NULL`.
    fn is_not_null(self) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::IsNull, SqlValue::Bool(false))
    }

    /// `column IN (v1, v2, …)`. Empty iterators are allowed at construction
    /// time; the SQL writer rejects them at compile time.
    fn is_in<V, I>(self, values: I) -> TypedFilter<Self::Model>
    where
        V: Into<Self::Value>,
        I: IntoIterator<Item = V>,
    {
        let list: Vec<SqlValue> = values.into_iter().map(|v| v.into().into()).collect();
        TypedFilter::scalar(Self::COLUMN, Op::In, SqlValue::List(list))
    }

    /// `SET column = value` for an UPDATE.
    fn set<V: Into<Self::Value>>(self, value: V) -> TypedAssignment<Self::Model> {
        TypedAssignment {
            inner: Assignment {
                column: Self::COLUMN,
                value: value.into().into(),
            },
            _model: PhantomData,
        }
    }
}

/// A `Filter` tagged with the model it applies to.
///
/// Constructed by [`Column`] methods. `QuerySet<M>::where_` accepts only
/// `TypedFilter<M>`, so you can't accidentally apply a filter from one
/// model to a queryset for another.
pub struct TypedFilter<M: Model> {
    pub(crate) inner: Filter,
    _model: PhantomData<fn() -> M>,
}

impl<M: Model> TypedFilter<M> {
    fn scalar(column: &'static str, op: Op, value: SqlValue) -> Self {
        Self {
            inner: Filter { column, op, value },
            _model: PhantomData,
        }
    }

    /// Unwrap to the dialect-neutral filter form.
    #[must_use]
    pub fn into_filter(self) -> Filter {
        self.inner
    }

    /// Compose with another predicate or expression via SQL `AND`.
    /// Returns a [`TypedExpr`] so further `.and()` / `.or()` calls
    /// can chain on the result.
    #[must_use]
    pub fn and<E: Into<TypedExpr<M>>>(self, rhs: E) -> TypedExpr<M> {
        TypedExpr::from(self).and(rhs)
    }

    /// Compose with another predicate or expression via SQL `OR`.
    /// Returns a [`TypedExpr`] so further `.and()` / `.or()` calls
    /// can chain on the result.
    #[must_use]
    pub fn or<E: Into<TypedExpr<M>>>(self, rhs: E) -> TypedExpr<M> {
        TypedExpr::from(self).or(rhs)
    }
}

impl<M: Model> From<TypedFilter<M>> for TypedExpr<M> {
    fn from(tf: TypedFilter<M>) -> Self {
        Self {
            inner: WhereExpr::Predicate(tf.inner),
            _model: PhantomData,
        }
    }
}

/// Typed boolean expression — a [`TypedFilter`] tree composed with
/// `.and()` / `.or()`. Constructed implicitly from any `TypedFilter`
/// via `Into`, so callers usually never name this type:
///
/// ```ignore
/// User::objects()
///     .where_(User::name.eq("alice").or(User::name.eq("bob")))
///     .where_(User::active.eq(true))
///     .fetch(&pool).await?;
/// // → WHERE ("name" = $1 OR "name" = $2) AND "active" = $3
/// ```
///
/// Each `.where_(…)` call AND-joins its expression into the
/// queryset's accumulated WHERE clause; OR is contained inside the
/// expression you pass to a single `.where_()` call.
pub struct TypedExpr<M: Model> {
    pub(crate) inner: WhereExpr,
    _model: PhantomData<fn() -> M>,
}

impl<M: Model> TypedExpr<M> {
    /// Unwrap to the dialect-neutral expression form.
    #[must_use]
    pub fn into_expr(self) -> WhereExpr {
        self.inner
    }

    /// Compose with `AND`. Adjacent `And` nodes are flattened so the
    /// resulting tree stays shallow:
    /// `a.and(b).and(c)` → `And(vec![a, b, c])`, not `And(And(a, b), c)`.
    #[must_use]
    pub fn and<E: Into<Self>>(self, rhs: E) -> Self {
        let rhs = rhs.into();
        let inner = match (self.inner, rhs.inner) {
            (WhereExpr::And(mut a), WhereExpr::And(b)) => {
                a.extend(b);
                WhereExpr::And(a)
            }
            (WhereExpr::And(mut a), b) => {
                a.push(b);
                WhereExpr::And(a)
            }
            (a, WhereExpr::And(mut b)) => {
                b.insert(0, a);
                WhereExpr::And(b)
            }
            (a, b) => WhereExpr::And(vec![a, b]),
        };
        Self {
            inner,
            _model: PhantomData,
        }
    }

    /// Compose with `OR`. Adjacent `Or` nodes are flattened so the
    /// resulting tree stays shallow.
    #[must_use]
    pub fn or<E: Into<Self>>(self, rhs: E) -> Self {
        let rhs = rhs.into();
        let inner = match (self.inner, rhs.inner) {
            (WhereExpr::Or(mut a), WhereExpr::Or(b)) => {
                a.extend(b);
                WhereExpr::Or(a)
            }
            (WhereExpr::Or(mut a), b) => {
                a.push(b);
                WhereExpr::Or(a)
            }
            (a, WhereExpr::Or(mut b)) => {
                b.insert(0, a);
                WhereExpr::Or(b)
            }
            (a, b) => WhereExpr::Or(vec![a, b]),
        };
        Self {
            inner,
            _model: PhantomData,
        }
    }
}

/// An `Assignment` tagged with the model it applies to.
pub struct TypedAssignment<M: Model> {
    pub(crate) inner: Assignment,
    _model: PhantomData<fn() -> M>,
}

impl<M: Model> TypedAssignment<M> {
    /// Unwrap to the dialect-neutral assignment form.
    #[must_use]
    pub fn into_assignment(self) -> Assignment {
        self.inner
    }
}