Skip to main content

turso_orm/entity/
model.rs

1//! Models and row decoding, modeled by [`ModelTrait`] and [`FromQueryResult`].
2//!
3//! A model is the plain, immutable snapshot of one row; the mutable,
4//! change-tracking counterpart lives in `active_model`. Decoding is driven
5//! by column name rather than position so that a model can be read out of
6//! any result set that happens to contain its columns, including the aliased
7//! `A_<col>` / `B_<col>` lists that `find_also_related` emits — that is why
8//! [`FromQueryResult::from_query_result`] takes a `prefix`. Tuples are the
9//! one positional exception: they decode the select list in order, which is
10//! what `into_tuple` relies on after `select_only`.
11//!
12//! [`ModelTrait::set`] returns a `Result` instead of panicking when the
13//! value does not decode as the field type, so that generic code can treat a
14//! bad value like any other error.
15
16use async_trait::async_trait;
17use turso_orm_driver::{ConnectionTrait, FromValue, Row};
18use turso_sql::Value;
19
20use super::active_model::{ActiveModelBehavior, ActiveModelTrait, IntoActiveModel};
21use super::base_entity::EntityTrait;
22use super::relation::{Linked, Related};
23use crate::query::{DeleteResult, Select};
24use crate::{DbErr, Result};
25
26/// A row of an entity as a plain struct, derived by `DeriveEntityModel`.
27#[async_trait]
28pub trait ModelTrait: Clone + Send + Sync + std::fmt::Debug {
29    /// The entity this model belongs to.
30    type Entity: EntityTrait<Model = Self>;
31
32    /// Reads a column as a SQL value.
33    fn get(&self, column: <Self::Entity as EntityTrait>::Column) -> Value;
34
35    /// Writes a column from a SQL value.
36    ///
37    /// # Errors
38    ///
39    /// Returns [`DbErr::Type`] when the value cannot be decoded as the field type.
40    fn set(&mut self, column: <Self::Entity as EntityTrait>::Column, value: Value) -> Result<()>;
41
42    /// Builds a query for the rows of `R` related to this model, following
43    /// the junction table of a many-to-many relation when there is one.
44    fn find_related<R>(&self, _: R) -> Select<R>
45    where
46        R: EntityTrait,
47        Self::Entity: Related<R>,
48    {
49        Select::<R>::find_related_to::<Self::Entity>(self)
50    }
51
52    /// Builds a query for the rows reached from this model through the
53    /// chain `link`.
54    fn find_linked<L>(&self, link: L) -> Select<L::ToEntity>
55    where
56        L: Linked<FromEntity = Self::Entity>,
57    {
58        Select::<L::ToEntity>::find_linked_to(&link, self)
59    }
60
61    /// Deletes the row this model was read from, running the active model's
62    /// [`ActiveModelBehavior`] hooks.
63    ///
64    /// # Errors
65    ///
66    /// Returns the errors of [`ActiveModelTrait::delete`].
67    async fn delete<C>(self, db: &C) -> Result<DeleteResult>
68    where
69        C: ConnectionTrait,
70        Self: IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
71        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior,
72    {
73        self.into_active_model().delete(db).await
74    }
75}
76
77/// A type that can be built from a result row.
78///
79/// Derived by `DeriveEntityModel` for models and by `FromQueryResult` for
80/// custom projection structs; implemented here for tuples, read by
81/// position, and for a JSON object, behind the `with-json` feature.
82pub trait FromQueryResult: Sized + Send + Sync {
83    /// Builds a value from `row`, reading columns named `{prefix}{column}`.
84    ///
85    /// The prefix is empty for plain selects and `A_` / `B_` for the two
86    /// sides of a `find_also_related` query.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`DbErr::Driver`] when a required column is missing from the
91    /// row or cannot be decoded as the field type.
92    fn from_query_result(row: &Row, prefix: &str) -> Result<Self>;
93
94    /// Like [`from_query_result`](Self::from_query_result), but returns
95    /// `Ok(None)` when every column carrying `prefix` is `NULL`.
96    ///
97    /// This is how the optional side of a `LEFT JOIN` is detected: a row
98    /// without a match has all of its `B_` columns `NULL`, which would
99    /// otherwise fail to decode into non-nullable fields.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`DbErr::Driver`] when a column is present but cannot be decoded.
104    fn from_query_result_optional(row: &Row, prefix: &str) -> Result<Option<Self>> {
105        let all_null = row
106            .iter()
107            .filter(|(name, _)| name.starts_with(prefix))
108            .all(|(_, v)| matches!(v, Value::Null));
109        if all_null {
110            Ok(None)
111        } else {
112            Self::from_query_result(row, prefix).map(Some)
113        }
114    }
115}
116
117impl FromQueryResult for Row {
118    fn from_query_result(row: &Row, _prefix: &str) -> Result<Self> {
119        Ok(row.clone())
120    }
121}
122
123/// Implements [`FromQueryResult`] for tuples, decoding the select list by
124/// position and ignoring the prefix.
125macro_rules! tuple_from_query_result {
126    ($(($($t:ident $i:tt),+));* $(;)?) => {$(
127        impl<$($t: FromValue + Send + Sync),+> FromQueryResult for ($($t,)+) {
128            fn from_query_result(row: &Row, _prefix: &str) -> Result<Self> {
129                Ok(($(row.get::<$t>($i)?,)+))
130            }
131        }
132    )*};
133}
134
135tuple_from_query_result! {
136    (A 0);
137    (A 0, B 1);
138    (A 0, B 1, C 2);
139    (A 0, B 1, C 2, D 3);
140    (A 0, B 1, C 2, D 3, E 4);
141    (A 0, B 1, C 2, D 3, E 4, F 5);
142}
143
144/// Decodes a row into a JSON object keyed by the column names without
145/// `prefix`.
146///
147/// Integers and reals become numbers, text a string, `NULL` null, and a
148/// blob an array of byte values, since JSON has no binary type.
149#[cfg(feature = "with-json")]
150#[cfg_attr(docsrs, doc(cfg(feature = "with-json")))]
151impl FromQueryResult for serde_json::Value {
152    fn from_query_result(row: &Row, prefix: &str) -> Result<Self> {
153        let mut object = serde_json::Map::new();
154        for (name, value) in row.iter() {
155            let Some(name) = name.strip_prefix(prefix) else {
156                continue;
157            };
158            let json = match value {
159                Value::Null => serde_json::Value::Null,
160                Value::Integer(n) => serde_json::Value::from(*n),
161                Value::Real(f) => serde_json::Value::from(*f),
162                Value::Text(s) => serde_json::Value::String(s.clone()),
163                Value::Blob(b) => serde_json::Value::Array(
164                    b.iter()
165                        .map(|byte| serde_json::Value::from(*byte))
166                        .collect(),
167                ),
168            };
169            object.insert(name.to_owned(), json);
170        }
171        Ok(serde_json::Value::Object(object))
172    }
173}
174
175/// Reads one column of `row` into a field type.
176///
177/// Used by generated `from_query_result` impls; the column is looked up as
178/// `{prefix}{column}` so the same impl serves plain and aliased selects.
179///
180/// # Errors
181///
182/// Returns [`DbErr::Driver`] when the column is missing from the row or
183/// cannot be decoded as `T`.
184pub fn get_field<T: FromValue>(row: &Row, prefix: &str, column: &str) -> Result<T> {
185    let name = if prefix.is_empty() {
186        column.to_owned()
187    } else {
188        format!("{prefix}{column}")
189    };
190    row.get::<T>(name.as_str()).map_err(DbErr::from)
191}