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 sqlx::{Database, FromRow};
7use std::future::Future;
8
9// Chapter 8: Weak Hook Pattern
10#[inline(never)]
11fn default_model_hook_result() -> Result<(), sqlx::Error> {
12    Ok(())
13}
14
15/// Hooks that can be implemented to run logic before or after database operations.
16pub trait ModelHooks {
17    /// Ran before a model is saved to the database.
18    #[inline(never)]
19    fn before_save(&mut self) -> impl Future<Output = Result<(), sqlx::Error>> + Send {
20        async move { default_model_hook_result() }
21    }
22    /// Ran after a model is successfully saved to the database.
23    #[inline(never)]
24    fn after_save(&mut self) -> impl Future<Output = Result<(), sqlx::Error>> + Send {
25        async move { default_model_hook_result() }
26    }
27}
28
29// Chapter 9: Optimistic Locking
30/// The result of an update operation, particularly relevant for optimistic locking.
31#[derive(Debug, PartialEq)]
32pub enum UpdateResult {
33    /// The update was successful.
34    Success,
35    /// The update failed due to a version mismatch (optimistic locking).
36    VersionConflict,
37    /// The record was not found.
38    NotFound,
39    /// The update operation is not implemented for this model.
40    NotImplemented,
41}
42
43/// Wrapper for fast positional row decoding.
44#[derive(Debug)]
45pub struct FastRow<DB, T>(T, std::marker::PhantomData<DB>);
46
47impl<DB, T> FastRow<DB, T> {
48    /// Extract the inner model.
49    pub fn into_inner(self) -> T {
50        self.0
51    }
52}
53
54impl<'r, DB, T> FromRow<'r, DB::Row> for FastRow<DB, T>
55where
56    DB: Database + SqlDialect,
57    T: Model<DB>,
58    usize: sqlx::ColumnIndex<DB::Row>,
59    for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
60{
61    fn from_row(row: &'r DB::Row) -> Result<Self, sqlx::Error> {
62        T::from_row_fast(row).map(|value| FastRow(value, std::marker::PhantomData))
63    }
64}
65
66// Chapter 10: Validation
67/// An error that occurred during model validation.
68#[derive(Debug, Clone)]
69pub struct ValidationError {
70    /// The name of the field that failed validation.
71    pub field: String,
72    /// A human-readable message describing the validation failure.
73    pub message: String,
74}
75
76/// A trait for validating model data before it is saved to the database.
77pub trait ModelValidation {
78    /// Validates the model. Returns `Err` with a list of validation errors if validation fails.
79    fn validate(&self) -> Result<(), Vec<ValidationError>> {
80        Ok(())
81    }
82}
83
84/// The core trait for database models.
85///
86/// This trait provides the foundation for all database interactions for a specific entity.
87/// It is usually implemented automatically via `#[derive(Model)]`.
88pub trait Model<DB: Database>: Sized + Send + Sync + Unpin
89where
90    DB: SqlDialect,
91    for<'r> Self: FromRow<'r, DB::Row>,
92{
93    /// Returns the name of the database table associated with this model.
94    fn table_name() -> &'static str;
95    /// Returns the SQL string required to create the table for this model.
96    fn create_table_sql() -> String;
97    /// Returns a list of column names for this model.
98    fn list_columns() -> Vec<String>;
99
100    /// Saves the current instance to the database.
101    fn save<'a, E>(
102        &'a mut self,
103        executor: E,
104    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
105    where
106        E: IntoExecutor<'a, DB = DB>;
107
108    /// Saves the current instance without hooks or extra safety checks.
109    fn save_fast<'a, E>(
110        &'a mut self,
111        executor: E,
112    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
113    where
114        E: IntoExecutor<'a, DB = DB>,
115    {
116        self.save(executor)
117    }
118
119    /// Saves the current instance using the ultra-fast path (no hooks/extra checks).
120    /// Note: On Postgres this may skip RETURNING and leave `id` unchanged.
121    fn save_ultra<'a, E>(
122        &'a mut self,
123        executor: E,
124    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
125    where
126        E: IntoExecutor<'a, DB = DB>,
127    {
128        self.save_fast(executor)
129    }
130
131    /// Updates the current instance in the database using optimistic locking if a `version` field exists.
132    fn update<'a, E>(
133        &'a mut self,
134        executor: E,
135    ) -> impl Future<Output = Result<UpdateResult, sqlx::Error>> + Send
136    where
137        E: IntoExecutor<'a, DB = DB>;
138
139    /// Updates the current instance without hooks or extra safety checks.
140    fn update_fast<'a, E>(
141        &'a mut self,
142        executor: E,
143    ) -> impl Future<Output = Result<UpdateResult, sqlx::Error>> + Send
144    where
145        E: IntoExecutor<'a, DB = DB>,
146    {
147        self.update(executor)
148    }
149
150    /// Updates the current instance using the ultra-fast path (no hooks/extra checks).
151    fn update_ultra<'a, E>(
152        &'a mut self,
153        executor: E,
154    ) -> impl Future<Output = Result<UpdateResult, sqlx::Error>> + Send
155    where
156        E: IntoExecutor<'a, DB = DB>,
157    {
158        self.update_fast(executor)
159    }
160
161    // Chapter 16: Soft Delete support
162    /// Deletes the current instance from the database (either hard or soft delete).
163    fn delete<'a, E>(
164        &'a mut self,
165        executor: E,
166    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
167    where
168        E: IntoExecutor<'a, DB = DB>;
169
170    /// Deletes the current instance without hooks or extra safety checks.
171    fn delete_fast<'a, E>(
172        &'a mut self,
173        executor: E,
174    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
175    where
176        E: IntoExecutor<'a, DB = DB>,
177    {
178        self.delete(executor)
179    }
180
181    /// Deletes the current instance using the ultra-fast path (no hooks/extra checks).
182    fn delete_ultra<'a, E>(
183        &'a mut self,
184        executor: E,
185    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send
186    where
187        E: IntoExecutor<'a, DB = DB>,
188    {
189        self.delete_fast(executor)
190    }
191    /// Returns whether this model supports soft deletes (via a `deleted_at` field).
192    fn has_soft_delete() -> bool;
193    /// Returns a list of fields that are considered sensitive and should be redacted in logs.
194    fn sensitive_fields() -> &'static [&'static str] {
195        &[]
196    }
197
198    /// Finds a record by its Primary Key.
199    fn find_by_id<'a, E>(
200        executor: E,
201        id: i32,
202    ) -> impl Future<Output = Result<Option<Self>, sqlx::Error>> + Send
203    where
204        E: IntoExecutor<'a, DB = DB>;
205
206    /// Fast row mapping using positional indices (override in derives for speed).
207    fn from_row_fast(row: &DB::Row) -> Result<Self, sqlx::Error>
208    where
209        usize: sqlx::ColumnIndex<DB::Row>,
210        for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
211        for<'r> Self: FromRow<'r, DB::Row>,
212    {
213        <Self as sqlx::FromRow<'_, DB::Row>>::from_row(row)
214    }
215
216    /// Use raw SQL and map rows into the current model type.
217    fn raw_sql<'q>(
218        sql: &'q str,
219    ) -> sqlx::query::QueryAs<'q, DB, Self, <DB as Database>::Arguments<'q>> {
220        sqlx::query_as::<DB, Self>(sql)
221    }
222
223    /// Use raw SQL with fast positional mapping (select columns in model field order).
224    fn raw_sql_fast<'q>(
225        sql: &'q str,
226    ) -> sqlx::query::QueryAs<'q, DB, crate::FastRow<DB, Self>, <DB as Database>::Arguments<'q>>
227    where
228        Self: Sized,
229        usize: sqlx::ColumnIndex<DB::Row>,
230        for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
231    {
232        sqlx::query_as::<DB, crate::FastRow<DB, Self>>(sql)
233    }
234
235    /// Loads related models for a list of instances.
236    #[inline(never)]
237    fn eager_load<'a>(
238        _models: &mut [Self],
239        _relation: &str,
240        _executor: Executor<'a, DB>,
241    ) -> impl Future<Output = Result<(), sqlx::Error>> + Send {
242        async move { default_model_hook_result() }
243    }
244    /// Creates a new [`QueryBuilder`] for this model.
245    fn find<'a, E>(executor: E) -> QueryBuilder<'a, Self, DB>
246    where
247        E: IntoExecutor<'a, DB = DB>,
248    {
249        QueryBuilder::new(executor.into_executor())
250    }
251
252    // Convenience helpers
253    /// Creates a new [`QueryBuilder`] using a connection pool.
254    fn find_in_pool(pool: &sqlx::Pool<DB>) -> QueryBuilder<'_, Self, DB> {
255        QueryBuilder::new(Executor::Pool(pool))
256    }
257
258    /// Creates a new [`QueryBuilder`] using an active database connection.
259    fn find_in_tx(conn: &mut DB::Connection) -> QueryBuilder<'_, Self, DB> {
260        QueryBuilder::new(Executor::Conn(conn))
261    }
262}
263
264/// Convenience helpers that map sqlx errors into `PremixError`.
265pub trait ModelResultExt<DB: Database>: Model<DB>
266where
267    DB: SqlDialect,
268    for<'r> Self: FromRow<'r, DB::Row>,
269{
270    /// Save the model and return `PremixError` on failure.
271    fn save_result<'a, E>(&'a mut self, executor: E) -> impl Future<Output = PremixResult<()>>
272    where
273        E: IntoExecutor<'a, DB = DB>;
274
275    /// Update the model and return `PremixError` on failure.
276    fn update_result<'a, E>(
277        &'a mut self,
278        executor: E,
279    ) -> impl Future<Output = PremixResult<UpdateResult>>
280    where
281        E: IntoExecutor<'a, DB = DB>;
282
283    /// Delete the model and return `PremixError` on failure.
284    fn delete_result<'a, E>(&'a mut self, executor: E) -> impl Future<Output = PremixResult<()>>
285    where
286        E: IntoExecutor<'a, DB = DB>;
287}
288
289impl<T, DB> ModelResultExt<DB> for T
290where
291    DB: SqlDialect,
292    T: Model<DB>,
293    for<'r> T: FromRow<'r, DB::Row>,
294{
295    #[allow(clippy::manual_async_fn)]
296    fn save_result<'a, E>(&'a mut self, executor: E) -> impl Future<Output = PremixResult<()>>
297    where
298        E: IntoExecutor<'a, DB = DB>,
299    {
300        async move { self.save(executor).await.map_err(PremixError::from) }
301    }
302
303    #[allow(clippy::manual_async_fn)]
304    fn update_result<'a, E>(
305        &'a mut self,
306        executor: E,
307    ) -> impl Future<Output = PremixResult<UpdateResult>>
308    where
309        E: IntoExecutor<'a, DB = DB>,
310    {
311        async move { self.update(executor).await.map_err(PremixError::from) }
312    }
313
314    #[allow(clippy::manual_async_fn)]
315    fn delete_result<'a, E>(&'a mut self, executor: E) -> impl Future<Output = PremixResult<()>>
316    where
317        E: IntoExecutor<'a, DB = DB>,
318    {
319        async move { self.delete(executor).await.map_err(PremixError::from) }
320    }
321}