turso-orm 0.1.6

An async ORM dedicated to the Turso database, with a typed entity API
//! Models and row decoding, modeled by [`ModelTrait`] and [`FromQueryResult`].
//!
//! A model is the plain, immutable snapshot of one row; the mutable,
//! change-tracking counterpart lives in `active_model`. Decoding is driven
//! by column name rather than position so that a model can be read out of
//! any result set that happens to contain its columns, including the aliased
//! `A_<col>` / `B_<col>` lists that `find_also_related` emits — that is why
//! [`FromQueryResult::from_query_result`] takes a `prefix`. Tuples are the
//! one positional exception: they decode the select list in order, which is
//! what `into_tuple` relies on after `select_only`.
//!
//! [`ModelTrait::set`] returns a `Result` instead of panicking when the
//! value does not decode as the field type, so that generic code can treat a
//! bad value like any other error.

use async_trait::async_trait;
use turso_orm_driver::{ConnectionTrait, FromValue, Row};
use turso_sql::Value;

use super::active_model::{ActiveModelBehavior, ActiveModelTrait, IntoActiveModel};
use super::base_entity::EntityTrait;
use super::relation::{Linked, Related};
use crate::query::{DeleteResult, Select};
use crate::{DbErr, Result};

/// A row of an entity as a plain struct, derived by `DeriveEntityModel`.
#[async_trait]
pub trait ModelTrait: Clone + Send + Sync + std::fmt::Debug {
    /// The entity this model belongs to.
    type Entity: EntityTrait<Model = Self>;

    /// Reads a column as a SQL value.
    fn get(&self, column: <Self::Entity as EntityTrait>::Column) -> Value;

    /// Writes a column from a SQL value.
    ///
    /// # Errors
    ///
    /// Returns [`DbErr::Type`] when the value cannot be decoded as the field type.
    fn set(&mut self, column: <Self::Entity as EntityTrait>::Column, value: Value) -> Result<()>;

    /// Builds a query for the rows of `R` related to this model, following
    /// the junction table of a many-to-many relation when there is one.
    fn find_related<R>(&self, _: R) -> Select<R>
    where
        R: EntityTrait,
        Self::Entity: Related<R>,
    {
        Select::<R>::find_related_to::<Self::Entity>(self)
    }

    /// Builds a query for the rows reached from this model through the
    /// chain `link`.
    fn find_linked<L>(&self, link: L) -> Select<L::ToEntity>
    where
        L: Linked<FromEntity = Self::Entity>,
    {
        Select::<L::ToEntity>::find_linked_to(&link, self)
    }

    /// Deletes the row this model was read from, running the active model's
    /// [`ActiveModelBehavior`] hooks.
    ///
    /// # Errors
    ///
    /// Returns the errors of [`ActiveModelTrait::delete`].
    async fn delete<C>(self, db: &C) -> Result<DeleteResult>
    where
        C: ConnectionTrait,
        Self: IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior,
    {
        self.into_active_model().delete(db).await
    }
}

/// A type that can be built from a result row.
///
/// Derived by `DeriveEntityModel` for models and by `FromQueryResult` for
/// custom projection structs; implemented here for tuples, read by
/// position, and for a JSON object, behind the `with-json` feature.
pub trait FromQueryResult: Sized + Send + Sync {
    /// Builds a value from `row`, reading columns named `{prefix}{column}`.
    ///
    /// The prefix is empty for plain selects and `A_` / `B_` for the two
    /// sides of a `find_also_related` query.
    ///
    /// # Errors
    ///
    /// Returns [`DbErr::Driver`] when a required column is missing from the
    /// row or cannot be decoded as the field type.
    fn from_query_result(row: &Row, prefix: &str) -> Result<Self>;

    /// Like [`from_query_result`](Self::from_query_result), but returns
    /// `Ok(None)` when every column carrying `prefix` is `NULL`.
    ///
    /// This is how the optional side of a `LEFT JOIN` is detected: a row
    /// without a match has all of its `B_` columns `NULL`, which would
    /// otherwise fail to decode into non-nullable fields.
    ///
    /// # Errors
    ///
    /// Returns [`DbErr::Driver`] when a column is present but cannot be decoded.
    fn from_query_result_optional(row: &Row, prefix: &str) -> Result<Option<Self>> {
        if all_null(row, prefix) {
            Ok(None)
        } else {
            Self::from_query_result(row, prefix).map(Some)
        }
    }

    /// Like [`from_query_result`](Self::from_query_result), but moves the
    /// values out of `row`, which is left with `NULL` in the taken columns.
    ///
    /// The derives implement this without copying text or blob values; the
    /// default delegates to the borrowing form, so a hand-written impl only
    /// needs that one. The query methods use this form on rows they own.
    ///
    /// # Errors
    ///
    /// Returns [`DbErr::Driver`] when a required column is missing from the
    /// row or cannot be decoded as the field type.
    fn from_query_result_mut(row: &mut Row, prefix: &str) -> Result<Self> {
        Self::from_query_result(row, prefix)
    }

    /// Like [`from_query_result_optional`](Self::from_query_result_optional),
    /// moving the values out of `row`.
    ///
    /// # Errors
    ///
    /// Returns [`DbErr::Driver`] when a column is present but cannot be decoded.
    fn from_query_result_optional_mut(row: &mut Row, prefix: &str) -> Result<Option<Self>> {
        if all_null(row, prefix) {
            Ok(None)
        } else {
            Self::from_query_result_mut(row, prefix).map(Some)
        }
    }
}

/// Whether every column of `row` carrying `prefix` is `NULL`: the mark of
/// the unmatched side of a `LEFT JOIN`.
fn all_null(row: &Row, prefix: &str) -> bool {
    row.iter()
        .filter(|(name, _)| name.starts_with(prefix))
        .all(|(_, v)| matches!(v, Value::Null))
}

impl FromQueryResult for Row {
    fn from_query_result(row: &Row, _prefix: &str) -> Result<Self> {
        Ok(row.clone())
    }
}

/// Implements [`FromQueryResult`] for tuples, decoding the select list by
/// position and ignoring the prefix.
macro_rules! tuple_from_query_result {
    ($(($($t:ident $i:tt),+));* $(;)?) => {$(
        impl<$($t: FromValue + Send + Sync),+> FromQueryResult for ($($t,)+) {
            fn from_query_result(row: &Row, _prefix: &str) -> Result<Self> {
                Ok(($(row.get::<$t>($i)?,)+))
            }
        }
    )*};
}

tuple_from_query_result! {
    (A 0);
    (A 0, B 1);
    (A 0, B 1, C 2);
    (A 0, B 1, C 2, D 3);
    (A 0, B 1, C 2, D 3, E 4);
    (A 0, B 1, C 2, D 3, E 4, F 5);
}

/// Decodes a row into a JSON object keyed by the column names without
/// `prefix`.
///
/// Integers and reals become numbers, text a string, `NULL` null, and a
/// blob an array of byte values, since JSON has no binary type.
#[cfg(feature = "with-json")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-json")))]
impl FromQueryResult for serde_json::Value {
    fn from_query_result(row: &Row, prefix: &str) -> Result<Self> {
        let mut object = serde_json::Map::new();
        for (name, value) in row.iter() {
            let Some(name) = name.strip_prefix(prefix) else {
                continue;
            };
            let json = match value {
                Value::Null => serde_json::Value::Null,
                Value::Integer(n) => serde_json::Value::from(*n),
                Value::Real(f) => serde_json::Value::from(*f),
                Value::Text(s) => serde_json::Value::String(s.clone()),
                Value::Blob(b) => serde_json::Value::Array(
                    b.iter()
                        .map(|byte| serde_json::Value::from(*byte))
                        .collect(),
                ),
            };
            object.insert(name.to_owned(), json);
        }
        Ok(serde_json::Value::Object(object))
    }
}

/// The stack buffer size for a prefixed column name in [`get_field`]: long
/// enough for every generated `A_` / `B_` / `__via_` alias of a realistic
/// column name.
const PREFIXED_NAME_CAPACITY: usize = 96;

/// Reads one column of `row` into a field type.
///
/// Used by generated `from_query_result` impls; the column is looked up as
/// `{prefix}{column}` so the same impl serves plain and aliased selects.
///
/// # Errors
///
/// Returns [`DbErr::Driver`] when the column is missing from the row or
/// cannot be decoded as `T`.
pub fn get_field<T: FromValue>(row: &Row, prefix: &str, column: &str) -> Result<T> {
    with_prefixed_name(prefix, column, |name| row.get::<T>(name)).map_err(DbErr::from)
}

/// Moves one column of `row` into a field type, leaving `NULL` behind.
///
/// The moving counterpart of [`get_field`], used by the generated
/// `from_query_result_mut` impls.
///
/// # Errors
///
/// Returns [`DbErr::Driver`] when the column is missing from the row or
/// cannot be decoded as `T`.
pub fn take_field<T: FromValue>(row: &mut Row, prefix: &str, column: &str) -> Result<T> {
    with_prefixed_name(prefix, column, |name| row.take::<T>(name)).map_err(DbErr::from)
}

/// Calls `f` with `{prefix}{column}` assembled without a heap allocation.
///
/// This runs once per field and per row, so a `String` here dominated the
/// decoding of a joined result set. The name is built in a stack buffer;
/// a name longer than the buffer is rare and falls back to a `String`.
fn with_prefixed_name<R>(prefix: &str, column: &str, f: impl FnOnce(&str) -> R) -> R {
    if prefix.is_empty() {
        return f(column);
    }
    let mut buf = [0u8; PREFIXED_NAME_CAPACITY];
    let len = prefix.len() + column.len();
    if len <= buf.len() {
        buf[..prefix.len()].copy_from_slice(prefix.as_bytes());
        buf[prefix.len()..len].copy_from_slice(column.as_bytes());
        // Both inputs are `str`, so their concatenation is valid UTF-8 and
        // the check cannot fail; the fallback below keeps the code total.
        if let Ok(name) = std::str::from_utf8(&buf[..len]) {
            return f(name);
        }
    }
    f(&format!("{prefix}{column}"))
}