Skip to main content

premix_core/
model.rs

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