Skip to main content

premix_core/
model.rs

1use crate::dialect::SqlDialect;
2use crate::error::{PremixError, PremixResult};
3use crate::executor::Executor;
4use crate::executor::IntoExecutor;
5use crate::query::QueryBuilder;
6use serde_json::Value;
7use sqlx::{Database, FromRow};
8use std::future::Future;
9
10// Chapter 8: Weak Hook Pattern
11#[inline(never)]
12fn default_model_hook_result() -> Result<(), sqlx::Error> {
13    Ok(())
14}
15
16/// Hooks that can be implemented to run logic before or after database operations.
17pub trait ModelHooks {
18    /// Ran before a model is saved to the database.
19    #[inline(never)]
20    fn before_save(&mut self) -> impl Future<Output = Result<(), sqlx::Error>> + Send {
21        async move { default_model_hook_result() }
22    }
23    /// Ran after a model is successfully saved to the database.
24    #[inline(never)]
25    fn after_save(&mut self) -> impl Future<Output = Result<(), sqlx::Error>> + Send {
26        async move { default_model_hook_result() }
27    }
28}
29
30// Chapter 9: Optimistic Locking
31/// The result of an update operation, particularly relevant for optimistic locking.
32#[derive(Debug, PartialEq)]
33pub enum UpdateResult {
34    /// The update was successful.
35    Success,
36    /// The update failed due to a version mismatch (optimistic locking).
37    VersionConflict,
38    /// The record was not found.
39    NotFound,
40    /// The update operation is not implemented for this model.
41    NotImplemented,
42}
43
44/// Wrapper for fast positional row decoding.
45#[derive(Debug)]
46pub struct FastRow<DB, T>(T, std::marker::PhantomData<DB>);
47
48impl<DB, T> FastRow<DB, T> {
49    /// Extract the inner model.
50    pub fn into_inner(self) -> T {
51        self.0
52    }
53}
54
55impl<'r, DB, T> FromRow<'r, DB::Row> for FastRow<DB, T>
56where
57    DB: Database + SqlDialect,
58    T: Model<DB>,
59    usize: sqlx::ColumnIndex<DB::Row>,
60    for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
61{
62    fn from_row(row: &'r DB::Row) -> Result<Self, sqlx::Error> {
63        T::from_row_fast(row).map(|value| FastRow(value, std::marker::PhantomData))
64    }
65}
66
67/// Typed relation handle for eager loading.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct Relation {
70    name: &'static str,
71}
72
73impl Relation {
74    /// Creates a new relation handle from a static name.
75    pub const fn new(name: &'static str) -> Self {
76        Self { name }
77    }
78
79    /// Returns the relation name.
80    pub const fn name(&self) -> &'static str {
81        self.name
82    }
83}
84
85impl From<Relation> for String {
86    fn from(value: Relation) -> Self {
87        value.name.to_string()
88    }
89}
90
91impl From<&Relation> for String {
92    fn from(value: &Relation) -> Self {
93        value.name.to_string()
94    }
95}
96
97// Chapter 10: Validation
98/// An error that occurred during model validation.
99#[derive(Debug, Clone)]
100pub struct ValidationError {
101    /// The name of the field that failed validation.
102    pub field: String,
103    /// A human-readable message describing the validation failure.
104    pub message: String,
105}
106
107/// A trait for validating model data before it is saved to the database.
108pub trait ModelValidation {
109    /// Validates the model. Returns `Err` with a list of validation errors if validation fails.
110    fn validate(&self) -> Result<(), Vec<ValidationError>> {
111        Ok(())
112    }
113}
114
115/// The core trait for database models.
116///
117/// This trait provides the foundation for all database interactions for a specific entity.
118/// It is usually implemented automatically via `#[derive(Model)]`.
119pub trait Model<DB: Database>: Sized + Send + Sync + Unpin
120where
121    DB: SqlDialect,
122    for<'r> Self: FromRow<'r, DB::Row>,
123{
124    /// Returns the name of the database table associated with this model.
125    fn table_name() -> &'static str;
126    /// Returns the SQL string required to create the table for this model.
127    fn create_table_sql() -> String;
128    /// Returns a list of column names for this model.
129    fn list_columns() -> Vec<String>;
130
131    /// Saves the current instance to the database.
132    fn save<'a, E>(
133        &'a mut self,
134        executor: E,
135    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
136    where
137        E: IntoExecutor<'a, DB = DB>;
138
139    /// Saves the current instance without hooks or extra safety checks.
140    fn save_fast<'a, E>(
141        &'a mut self,
142        executor: E,
143    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
144    where
145        E: IntoExecutor<'a, DB = DB>,
146    {
147        self.save(executor)
148    }
149
150    /// Saves the current instance using the ultra-fast path (no hooks/extra checks).
151    /// Note: On Postgres this may skip RETURNING and leave `id` unchanged.
152    fn save_ultra<'a, E>(
153        &'a mut self,
154        executor: E,
155    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
156    where
157        E: IntoExecutor<'a, DB = DB>,
158    {
159        self.save_fast(executor)
160    }
161
162    /// Updates the current instance in the database using optimistic locking if a `version` field exists.
163    fn update<'a, E>(
164        &'a mut self,
165        executor: E,
166    ) -> impl Future<Output = Result<UpdateResult, sqlx::Error>> + Send
167    where
168        E: IntoExecutor<'a, DB = DB>;
169
170    /// Updates the current instance without hooks or extra safety checks.
171    fn update_fast<'a, E>(
172        &'a mut self,
173        executor: E,
174    ) -> impl Future<Output = Result<UpdateResult, sqlx::Error>> + Send
175    where
176        E: IntoExecutor<'a, DB = DB>,
177    {
178        self.update(executor)
179    }
180
181    /// Updates the current instance using the ultra-fast path (no hooks/extra checks).
182    fn update_ultra<'a, E>(
183        &'a mut self,
184        executor: E,
185    ) -> impl Future<Output = Result<UpdateResult, sqlx::Error>> + Send
186    where
187        E: IntoExecutor<'a, DB = DB>,
188    {
189        self.update_fast(executor)
190    }
191
192    // Chapter 16: Soft Delete support
193    /// Deletes the current instance from the database (either hard or soft delete).
194    fn delete<'a, E>(
195        &'a mut self,
196        executor: E,
197    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
198    where
199        E: IntoExecutor<'a, DB = DB>;
200
201    /// Deletes the current instance without hooks or extra safety checks.
202    fn delete_fast<'a, E>(
203        &'a mut self,
204        executor: E,
205    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
206    where
207        E: IntoExecutor<'a, DB = DB>,
208    {
209        self.delete(executor)
210    }
211
212    /// Deletes the current instance using the ultra-fast path (no hooks/extra checks).
213    fn delete_ultra<'a, E>(
214        &'a mut self,
215        executor: E,
216    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
217    where
218        E: IntoExecutor<'a, DB = DB>,
219    {
220        self.delete_fast(executor)
221    }
222    /// Returns whether this model supports soft deletes (via a `deleted_at` field).
223    fn has_soft_delete() -> bool;
224    /// Returns a list of fields that are considered sensitive and should be redacted in logs.
225    fn sensitive_fields() -> &'static [&'static str] {
226        &[]
227    }
228
229    /// Returns the relation names available for eager loading.
230    fn relation_names() -> &'static [&'static str] {
231        &[]
232    }
233
234    /// Returns relations that should be eager-loaded by default.
235    fn default_includes() -> &'static [&'static str] {
236        &[]
237    }
238
239    /// Finds a record by its Primary Key.
240    fn find_by_id<'a, E>(
241        executor: E,
242        id: i32,
243    ) -> impl Future<Output = Result<Option<Self>, sqlx::Error>> + Send
244    where
245        E: IntoExecutor<'a, DB = DB>;
246
247    /// Fast row mapping using positional indices (override in derives for speed).
248    fn from_row_fast(row: &DB::Row) -> Result<Self, sqlx::Error>
249    where
250        usize: sqlx::ColumnIndex<DB::Row>,
251        for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
252        for<'r> Self: FromRow<'r, DB::Row>,
253    {
254        <Self as sqlx::FromRow<'_, DB::Row>>::from_row(row)
255    }
256
257    /// Use raw SQL and map rows into the current model type.
258    fn raw_sql<'q>(
259        sql: &'q str,
260    ) -> sqlx::query::QueryAs<'q, DB, Self, <DB as Database>::Arguments<'q>> {
261        sqlx::query_as::<DB, Self>(sql)
262    }
263
264    /// Use raw SQL with fast positional mapping (select columns in model field order).
265    fn raw_sql_fast<'q>(
266        sql: &'q str,
267    ) -> sqlx::query::QueryAs<'q, DB, crate::FastRow<DB, Self>, <DB as Database>::Arguments<'q>>
268    where
269        Self: Sized,
270        usize: sqlx::ColumnIndex<DB::Row>,
271        for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
272    {
273        sqlx::query_as::<DB, crate::FastRow<DB, Self>>(sql)
274    }
275
276    /// Loads related models for a list of instances.
277    #[inline(never)]
278    fn eager_load<'a>(
279        _models: &mut [Self],
280        _relation: &str,
281        _executor: Executor<'a, DB>,
282    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send {
283        async move { default_model_hook_result() }
284    }
285    /// Creates a new [`QueryBuilder`] for this model.
286    fn find<'a, E>(executor: E) -> QueryBuilder<'a, Self, DB>
287    where
288        E: IntoExecutor<'a, DB = DB>,
289    {
290        QueryBuilder::new(executor.into_executor())
291    }
292
293    /// Fetches all records for this model.
294    fn all<'a, E>(executor: E) -> impl Future<Output = Result<Vec<Self>, sqlx::Error>> + Send
295    where
296        E: IntoExecutor<'a, DB = DB>,
297        for<'q> <DB as Database>::Arguments<'q>: sqlx::IntoArguments<'q, DB>,
298        for<'c> &'c mut <DB as Database>::Connection: sqlx::Executor<'c, Database = DB>,
299        for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
300        DB::Connection: Send,
301        Self: Send,
302        String: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
303        i64: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
304        f64: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
305        bool: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
306        Option<String>: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
307        uuid::Uuid: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
308        chrono::DateTime<chrono::Utc>: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
309        chrono::NaiveDateTime: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
310        chrono::NaiveDate: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
311        sqlx::types::Json<serde_json::Value>: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
312    {
313        async move { Self::find(executor).all().await }
314    }
315
316    /// Finds a record by its primary key (alias for [`find_by_id`]).
317    fn find_one<'a, E>(
318        executor: E,
319        id: i32,
320    ) -> impl Future<Output = Result<Option<Self>, sqlx::Error>> + Send
321    where
322        E: IntoExecutor<'a, DB = DB>,
323    {
324        Self::find_by_id(executor, id)
325    }
326
327    /// Creates a new record and returns the saved instance.
328    fn create<E>(
329        executor: E,
330        mut instance: Self,
331    ) -> impl Future<Output = Result<Self, sqlx::Error>> + Send
332    where
333        for<'e> E: IntoExecutor<'e, DB = DB> + Send,
334    {
335        async move {
336            instance.save(executor).await?;
337            Ok(instance)
338        }
339    }
340
341    /// Applies a JSON patch update by primary key.
342    fn update_by_id<'a, E>(
343        executor: E,
344        id: i32,
345        json_patch: Value,
346    ) -> impl Future<Output = Result<u64, sqlx::Error>> + Send
347    where
348        E: IntoExecutor<'a, DB = DB>,
349        for<'q> <DB as Database>::Arguments<'q>: sqlx::IntoArguments<'q, DB>,
350        for<'c> &'c mut <DB as Database>::Connection: sqlx::Executor<'c, Database = DB>,
351        for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
352        DB::Connection: Send,
353        Self: Send,
354        String: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
355        i64: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
356        f64: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
357        bool: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
358        Option<String>: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
359        uuid::Uuid: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
360        chrono::DateTime<chrono::Utc>: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
361        chrono::NaiveDateTime: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
362        chrono::NaiveDate: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
363        sqlx::types::Json<serde_json::Value>: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
364    {
365        async move {
366            Self::find(executor)
367                .filter_eq("id", id)
368                .update(json_patch)
369                .await
370        }
371    }
372
373    // Convenience helpers
374    /// Creates a new [`QueryBuilder`] using a connection pool.
375    fn find_in_pool(pool: &sqlx::Pool<DB>) -> QueryBuilder<'_, Self, DB> {
376        QueryBuilder::new(Executor::Pool(pool))
377    }
378
379    /// Creates a new [`QueryBuilder`] using an active database connection.
380    fn find_in_tx(conn: &mut DB::Connection) -> QueryBuilder<'_, Self, DB> {
381        QueryBuilder::new(Executor::Conn(conn))
382    }
383}
384
385/// Convenience helpers that map sqlx errors into `PremixError`.
386pub trait ModelResultExt<DB: Database>: Model<DB>
387where
388    DB: SqlDialect,
389    for<'r> Self: FromRow<'r, DB::Row>,
390{
391    /// Save the model and return `PremixError` on failure.
392    fn save_result<'a, E>(&'a mut self, executor: E) -> impl Future<Output = PremixResult<()>>
393    where
394        E: IntoExecutor<'a, DB = DB>;
395
396    /// Update the model and return `PremixError` on failure.
397    fn update_result<'a, E>(
398        &'a mut self,
399        executor: E,
400    ) -> impl Future<Output = PremixResult<UpdateResult>>
401    where
402        E: IntoExecutor<'a, DB = DB>;
403
404    /// Delete the model and return `PremixError` on failure.
405    fn delete_result<'a, E>(&'a mut self, executor: E) -> impl Future<Output = PremixResult<()>>
406    where
407        E: IntoExecutor<'a, DB = DB>;
408}
409
410impl<T, DB> ModelResultExt<DB> for T
411where
412    DB: SqlDialect,
413    T: Model<DB>,
414    for<'r> T: FromRow<'r, DB::Row>,
415{
416    #[allow(clippy::manual_async_fn)]
417    fn save_result<'a, E>(&'a mut self, executor: E) -> impl Future<Output = PremixResult<()>>
418    where
419        E: IntoExecutor<'a, DB = DB>,
420    {
421        async move { self.save(executor).await.map_err(PremixError::from) }
422    }
423
424    #[allow(clippy::manual_async_fn)]
425    fn update_result<'a, E>(
426        &'a mut self,
427        executor: E,
428    ) -> impl Future<Output = PremixResult<UpdateResult>>
429    where
430        E: IntoExecutor<'a, DB = DB>,
431    {
432        async move { self.update(executor).await.map_err(PremixError::from) }
433    }
434
435    #[allow(clippy::manual_async_fn)]
436    fn delete_result<'a, E>(&'a mut self, executor: E) -> impl Future<Output = PremixResult<()>>
437    where
438        E: IntoExecutor<'a, DB = DB>,
439    {
440        async move { self.delete(executor).await.map_err(PremixError::from) }
441    }
442}