rustango 0.27.3

Django-shaped batteries-included web framework for Rust: ORM + migrations + auto-admin + multi-tenancy + audit log + auth (sessions, JWT, OAuth2/OIDC, HMAC) + APIs (ViewSet, OpenAPI auto-derive, JSON:API) + jobs (in-mem + Postgres) + email + media (S3 / R2 / B2 / MinIO + presigned uploads + collections + tags) + production middleware (CSRF, CSP, rate-limiting, compression, idempotency, etc.).
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.
    fn like<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::Like, value.into().into())
    }

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

    /// `column ILIKE value` — case-insensitive (Postgres).
    fn ilike<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::ILike, value.into().into())
    }

    /// `column NOT ILIKE value` — case-insensitive (Postgres).
    fn not_ilike<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::NotILike, 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))
    }

    /// `column NOT IN (v1, v2, …)`.
    fn not_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::NotIn, SqlValue::List(list))
    }

    /// `column BETWEEN lo AND hi`. Both bounds are inclusive.
    fn between<V: Into<Self::Value>>(self, lo: V, hi: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(
            Self::COLUMN,
            Op::Between,
            SqlValue::List(vec![lo.into().into(), hi.into().into()]),
        )
    }

    /// `column IS DISTINCT FROM value` — null-safe inequality.
    fn is_distinct_from<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::IsDistinctFrom, value.into().into())
    }

    /// `column IS NOT DISTINCT FROM value` — null-safe equality.
    fn is_not_distinct_from<V: Into<Self::Value>>(self, value: V) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::IsNotDistinctFrom, value.into().into())
    }

    /// JSONB `@>` — column contains the given JSON value.
    /// Bind a `serde_json::Value`.
    fn json_contains(self, value: serde_json::Value) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::JsonContains, SqlValue::Json(value))
    }

    /// JSONB `<@` — column is contained by the given JSON value.
    fn json_contained_by(self, value: serde_json::Value) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::JsonContainedBy, SqlValue::Json(value))
    }

    /// JSONB `?` — the key exists as a top-level key in the column.
    fn json_has_key(self, key: impl Into<String>) -> TypedFilter<Self::Model> {
        TypedFilter::scalar(Self::COLUMN, Op::JsonHasKey, SqlValue::String(key.into()))
    }

    /// JSONB `?|` — any of the given keys exist as top-level keys.
    fn json_has_any_key<I: IntoIterator<Item = impl Into<String>>>(
        self,
        keys: I,
    ) -> TypedFilter<Self::Model> {
        let list: Vec<SqlValue> = keys
            .into_iter()
            .map(|k| SqlValue::String(k.into()))
            .collect();
        TypedFilter::scalar(Self::COLUMN, Op::JsonHasAnyKey, SqlValue::List(list))
    }

    /// JSONB `?&` — all of the given keys exist as top-level keys.
    fn json_has_all_keys<I: IntoIterator<Item = impl Into<String>>>(
        self,
        keys: I,
    ) -> TypedFilter<Self::Model> {
        let list: Vec<SqlValue> = keys
            .into_iter()
            .map(|k| SqlValue::String(k.into()))
            .collect();
        TypedFilter::scalar(Self::COLUMN, Op::JsonHasAllKeys, 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)
    }

    /// Negate this predicate — emits `NOT (col op val)`.
    #[must_use]
    pub fn not(self) -> TypedExpr<M> {
        TypedExpr::from(self).not()
    }
}

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,
        }
    }

    /// Negate this expression — emits `NOT (…)`.
    #[must_use]
    pub fn not(self) -> Self {
        Self {
            inner: WhereExpr::Not(Box::new(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
    }
}