rango-orm-core 0.2.0

Database-free core of Rango ORM: model traits, schema and field types
Documentation
//! Column and ordering types for Rango ORM.

use std::marker::PhantomData;

use crate::Model;
use crate::fields::ForeignKey;
use crate::query::{SqlValue, ToSqlValue};

/// A typed column reference: `M` is the model, `T` the field type.
///
/// `#[derive(Model)]` generates one per field (`User::EMAIL`), so filters check
/// both the column name and the value type at compile time.
pub struct Column<M, T> {
    name: &'static str,
    _marker: PhantomData<fn() -> (M, T)>,
}

impl<M, T> Copy for Column<M, T> {}

impl<M, T> Clone for Column<M, T> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<M, T> std::fmt::Debug for Column<M, T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("Column({:?})", self.name))
    }
}

impl<M, T> Column<M, T> {
    /// Used by `#[derive(Model)]`; you rarely call it directly.
    pub const fn new(name: &'static str) -> Self {
        Self {
            name,
            _marker: PhantomData,
        }
    }

    /// Returns the column name.
    pub const fn name(&self) -> &'static str {
        self.name
    }

    /// Returns an ascending ordering for this column.
    pub const fn asc(self) -> Ordering<M> {
        Ordering::new(self.name, false)
    }

    /// Returns a descending ordering for this column.
    pub const fn desc(self) -> Ordering<M> {
        Ordering::new(self.name, true)
    }
}

/// An ordering for a column.
pub struct Ordering<M> {
    name: &'static str,
    descending: bool,
    _model: PhantomData<fn() -> M>,
}

impl<M> Ordering<M> {
    const fn new(name: &'static str, descending: bool) -> Self {
        Self {
            name,
            descending,
            _model: PhantomData,
        }
    }
}

impl<M> Copy for Ordering<M> {}

impl<M> Clone for Ordering<M> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<M> std::fmt::Debug for Ordering<M> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!(
            "Ordering({:?}, descending={})",
            self.name, self.descending
        ))
    }
}

/// A column reference accepted by query builder methods: either a typed `Column<M, T>`
/// generated by `#[derive(Model)]` (checked at compile time) or a `&str`
/// (checked by the database at runtime).
pub trait ColumnRef<M> {
    fn column_name(&self) -> &str;
}

impl<M, T> ColumnRef<M> for Column<M, T> {
    fn column_name(&self) -> &str {
        self.name
    }
}

impl<M> ColumnRef<M> for &str {
    fn column_name(&self) -> &str {
        self
    }
}

/// A foreign key column on `M` pointing to `T`: `Post::AUTHOR_ID` (an `author_id: ForeignKey<User>` field)
/// is a `ForeignKeyRef<Post, User>`, so relation methods infer the related model and reject
/// a column that points elsewhere. `&str` is accepted too (then name the model with `::<T>`).
pub trait ForeignKeyRef<M, T>: ColumnRef<M> {}

impl<M, T: Model> ForeignKeyRef<M, T> for Column<M, ForeignKey<T>> {}

impl<M, T> ForeignKeyRef<M, T> for &str {}

/// A `ManyToMany<R>` field on `M`: `#[derive(Model)]` generates `Article::TAGS` for a
/// `tags: ManyToMany<Tag>` field. Not a column, so it only works with `prefetch_related`.
pub struct ManyToManyField<M, R>(PhantomData<fn() -> (M, R)>);

impl<M, R> ManyToManyField<M, R> {
    /// Used by `#[derive(Model)]`; you rarely call it directly.
    pub const fn new() -> Self {
        Self(PhantomData)
    }
}

impl<M, R> Default for ManyToManyField<M, R> {
    fn default() -> Self {
        Self::new()
    }
}

/// How `prefetch_related` finds the `R` rows of each `M`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrefetchKind<'a> {
    /// A column on R's table pointing back to M's primary key.
    ReverseFk(&'a str),
    /// Through the M2M pivot table (see [`crate::related::m2m_pivot`]).
    ManyToMany,
}

/// Accepted by `prefetch_related`: a reverse foreign key (`Comment::POST_ID`, a
/// `post_id: ForeignKey<Post>` field on `Comment`), a M2M field (`Article::TAGS`), or `&str`
/// naming the reverse FK column (then name the model with `::<R>`).
pub trait PrefetchRef<M, R> {
    fn prefetch_kind(&self) -> PrefetchKind<'_>;
}

impl<M: Model, R> PrefetchRef<M, R> for Column<R, ForeignKey<M>> {
    fn prefetch_kind(&self) -> PrefetchKind<'_> {
        PrefetchKind::ReverseFk(self.name)
    }
}

impl<M, R> PrefetchRef<M, R> for &str {
    fn prefetch_kind(&self) -> PrefetchKind<'_> {
        PrefetchKind::ReverseFk(self)
    }
}

impl<M, R> PrefetchRef<M, R> for ManyToManyField<M, R> {
    fn prefetch_kind(&self) -> PrefetchKind<'_> {
        PrefetchKind::ManyToMany
    }
}

/// Accepted by `order_by`: `Column<M, T>` (ascending), `Ordering<M>` (from `.asc()` / `.desc()`),
/// or `&str` where a leading `-` means descending (Django style).
pub trait OrderRef<M> {
    fn order_parts(&self) -> (&str, bool);
}

impl<M, T> OrderRef<M> for Column<M, T> {
    fn order_parts(&self) -> (&str, bool) {
        (self.name, false)
    }
}

impl<M> OrderRef<M> for Ordering<M> {
    fn order_parts(&self) -> (&str, bool) {
        (self.name, self.descending)
    }
}

impl<M> OrderRef<M> for &str {
    fn order_parts(&self) -> (&str, bool) {
        match self.strip_prefix("-") {
            Some(name) => (name, true),
            None => (*self, false),
        }
    }
}

/// A value that can be compared to a column of field type `T`:
/// the field type itself, or its plain Rust equivalent (`i32` for `FieldInt`,
/// `&str` for `FieldEmail`, ...). Filter values are not validated like field
/// constructors are: `"not-an-email"` is a valid value to search for.
#[diagnostic::on_unimplemented(
    message = "`{Self}` cannot be compared to a `{T}` column",
    label = "expected a `{T}` value or its plain Rust equivalent"
)]
pub trait FilterValue<T> {
    fn into_sql_value(self) -> SqlValue;
}

#[diagnostic::do_not_recommend]
impl<T: ToSqlValue> FilterValue<T> for T {
    fn into_sql_value(self) -> SqlValue {
        self.to_sql_value()
    }
}

/// A column accepted by comparison filters (`eq`, `gt`, ...) together with its value type `V`.
///
/// `Column<M, T>` only accepts values of its field type (`.eq(User::AGE, "x")` does
/// not compile); `&str` accepts anything convertible to `SqlValue`.
#[diagnostic::on_unimplemented(
    message = "`{Self}` is not a column of this model that accepts a `{V}` value",
    note = "pass a typed column such as `Model::NAME` with a value of its field type, or a column name as `&str`"
)]
pub trait FilterColumn<M, V>: ColumnRef<M> {
    fn bind(&self, val: V) -> SqlValue;
}

#[diagnostic::do_not_recommend]
impl<M, T, V: FilterValue<T>> FilterColumn<M, V> for Column<M, T> {
    fn bind(&self, val: V) -> SqlValue {
        val.into_sql_value()
    }
}

#[diagnostic::do_not_recommend]
impl<M, V: Into<SqlValue>> FilterColumn<M, V> for &str {
    fn bind(&self, val: V) -> SqlValue {
        val.into()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fields::{FieldEmail, FieldInt, FieldText};

    struct User;

    #[test]
    fn filter_values_are_typed() {
        let age = Column::<User, FieldInt>::new("age");
        assert_eq!(age.bind(18), SqlValue::Int(18));
        assert_eq!(age.bind(FieldInt(18)), SqlValue::Int(18));
        let email = Column::<User, FieldEmail>::new("email");
        assert_eq!(email.bind("a@b.c"), SqlValue::Text("a@b.c".into()));
        assert_eq!(
            FilterColumn::<User, _>::bind(&"age", 18i64),
            SqlValue::BigInt(18)
        );
    }

    #[test]
    fn column_name() {
        assert_eq!(
            Column::<User, FieldText>::new("email").column_name(),
            "email"
        );
        assert_eq!(ColumnRef::<User>::column_name(&"email"), "email");
    }

    #[test]
    fn order_parts() {
        let column = Column::<User, FieldText>::new("name");
        assert_eq!(column.order_parts(), ("name", false));
        assert_eq!(column.desc().order_parts(), ("name", true));
        assert_eq!(column.asc().order_parts(), ("name", false));
        assert_eq!(OrderRef::<User>::order_parts(&"-name"), ("name", true));
        assert_eq!(OrderRef::<User>::order_parts(&"name"), ("name", false));
    }

    fn assert_traits<T: Send + Sync + Copy>() {}

    #[test]
    fn column_is_send_sync_copy() {
        assert_traits::<Column<*const u8, *const u8>>();
    }
}