Skip to main content

rws_server/model/
mod.rs

1//! # Model — Async Database Access Layer
2//!
3//! An async JPA/Hibernate-style ORM backed by [sqlx]. Provides:
4//! - `#[derive(Model)]` proc-macro for struct-to-table mapping
5//! - `Repository<T, i64>` trait for zero-boilerplate async CRUD
6//! - `QueryBuilder<T>` for fluent async filtering
7//! - `DbPool` for connection management (wraps `sqlx::Pool`)
8//! - Async migration runner
9//! - Relationship helpers (`HasMany`, `HasOne`, `BelongsTo`)
10//! - Primary keys of type `i64`, `i32`, `String` or [`Uuid`] (18.30.0)
11//! - Conventions `save` and `delete` maintain (18.30.0): `#[timestamps]`
12//!   (`created_at` / `updated_at`, [`Timestamp`]), `#[soft_delete]`
13//!   (`deleted_at`; deleted rows are left out of queries unless asked for)
14//!   and `#[version]` (optimistic locking: saving a stale copy fails with
15//!   [`DbErrorKind::StaleVersion`] instead of overwriting)
16//!
17//! Enable one or more backend features — `model-sqlite`, `model-postgres`,
18//! and `model-mysql` are not mutually exclusive, so a single binary can hold
19//! pools to more than one backend at once (see [`Backend`]):
20//!
21//! ```toml
22//! # SQLite
23//! rust-web-server = { features = ["model-sqlite"] }
24//! # PostgreSQL
25//! rust-web-server = { features = ["model-postgres"] }
26//! # MySQL
27//! rust-web-server = { features = ["model-mysql"] }
28//! # Or more than one at once
29//! rust-web-server = { features = ["model-sqlite", "model-postgres"] }
30//! ```
31
32pub mod backend;
33pub mod connection;
34pub mod migration;
35pub mod pool;
36pub mod query;
37pub mod relation;
38pub mod repository;
39pub mod timestamp;
40mod decode;
41pub mod uuid;
42#[cfg(test)]
43mod tests;
44#[cfg(test)]
45mod backends;
46#[cfg(test)]
47mod convention_tests;
48#[cfg(test)]
49mod tls_tests;
50
51pub use backend::Backend;
52pub use connection::{DbConfig, DbSslMode, DbTls};
53pub use migration::MigrationStatus;
54pub use pool::{DbPool, DbTransaction};
55pub use query::{Order, QueryBuilder};
56pub use relation::{BelongsTo, HasMany, HasOne};
57pub use repository::{ModelRepository, PrimaryKey, Repository};
58pub use timestamp::Timestamp;
59pub use uuid::Uuid;
60pub use crate::pagination::{CursorPage, Page};
61
62// ── Value ─────────────────────────────────────────────────────────────────────
63
64/// Backend-independent SQL value representation.
65#[derive(Debug, Clone, PartialEq)]
66pub enum Value {
67    Null,
68    Bool(bool),
69    Int(i64),
70    Float(f64),
71    Text(String),
72    Bytes(Vec<u8>),
73}
74
75// ── DbError ───────────────────────────────────────────────────────────────────
76
77/// Error type returned by all model/DB operations.
78///
79/// The message says what failed. Errors a caller handles differently —
80/// a stale optimistic-locking version, a row that isn't there — have a
81/// [`kind`](DbError::kind) too (18.30.0); it is carried by a fixed message
82/// prefix (`stale version: `, `not found: `), since `DbError` stays a
83/// tuple struct of its message until a major release.
84#[derive(Debug)]
85pub struct DbError(pub String);
86
87impl std::fmt::Display for DbError {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(f, "DbError: {}", self.0)
90    }
91}
92
93impl std::error::Error for DbError {}
94
95/// What a [`DbError`] is about, for errors a caller handles differently
96/// from the rest (18.30.0).
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum DbErrorKind {
100    /// `save` of a `#[version]`ed model whose version no longer matches the
101    /// row's: someone saved it since this copy was read. Reload and retry,
102    /// or tell the user.
103    StaleVersion,
104    /// The row to update or restore doesn't exist.
105    NotFound,
106    /// Anything else: SQL errors, connection errors, conversions.
107    Other,
108}
109
110const STALE_VERSION: &str = "stale version: ";
111const NOT_FOUND: &str = "not found: ";
112
113impl DbError {
114    pub fn new(msg: impl Into<String>) -> Self {
115        DbError(msg.into())
116    }
117
118    /// A [`DbErrorKind::StaleVersion`] error; `detail` says which row and
119    /// versions.
120    pub fn stale_version(detail: impl std::fmt::Display) -> Self {
121        DbError(format!("{STALE_VERSION}{detail}"))
122    }
123
124    /// A [`DbErrorKind::NotFound`] error.
125    pub fn not_found(detail: impl std::fmt::Display) -> Self {
126        DbError(format!("{NOT_FOUND}{detail}"))
127    }
128
129    /// What this error is about.
130    ///
131    /// ```rust
132    /// # extern crate rws_server as rust_web_server;
133    /// use rust_web_server::model::{DbError, DbErrorKind};
134    ///
135    /// let e = DbError::stale_version("users id=1: the row is at version 3, this copy at 2");
136    /// assert_eq!(e.kind(), DbErrorKind::StaleVersion);
137    /// assert_eq!(DbError::new("syntax error").kind(), DbErrorKind::Other);
138    /// ```
139    pub fn kind(&self) -> DbErrorKind {
140        if self.0.starts_with(STALE_VERSION) {
141            DbErrorKind::StaleVersion
142        } else if self.0.starts_with(NOT_FOUND) {
143            DbErrorKind::NotFound
144        } else {
145            DbErrorKind::Other
146        }
147    }
148
149    /// `kind() == DbErrorKind::StaleVersion`.
150    pub fn is_stale_version(&self) -> bool {
151        self.kind() == DbErrorKind::StaleVersion
152    }
153
154    /// `kind() == DbErrorKind::NotFound`.
155    pub fn is_not_found(&self) -> bool {
156        self.kind() == DbErrorKind::NotFound
157    }
158}
159
160// ── ModelRow ──────────────────────────────────────────────────────────────────
161
162/// Backend-independent database row.
163#[derive(Debug, Clone)]
164pub struct ModelRow {
165    #[doc(hidden)]
166    pub columns: Vec<(String, Value)>,
167}
168
169impl ModelRow {
170    pub fn new(columns: Vec<(String, Value)>) -> Self {
171        ModelRow { columns }
172    }
173
174    /// Retrieve and convert a column value by name.
175    pub fn get<T: FromColumn>(&self, col: &str) -> Result<T, DbError> {
176        for (name, val) in &self.columns {
177            if name.eq_ignore_ascii_case(col) {
178                return T::from_column(val.clone());
179            }
180        }
181        Err(DbError::new(format!("column '{}' not found in row", col)))
182    }
183}
184
185// ── FromColumn / ToColumn ─────────────────────────────────────────────────────
186
187/// Convert a `Value` into a Rust type.
188pub trait FromColumn: Sized {
189    fn from_column(v: Value) -> Result<Self, DbError>;
190}
191
192/// Convert a Rust value into a `Value`.
193pub trait ToColumn {
194    fn to_column(&self) -> Value;
195}
196
197// ── FromColumn impls ──────────────────────────────────────────────────────────
198
199impl FromColumn for i16 {
200    fn from_column(v: Value) -> Result<Self, DbError> {
201        match v {
202            Value::Int(n) => Ok(n as i16),
203            Value::Null => Err(DbError::new("unexpected NULL for i16")),
204            other => Err(DbError::new(format!("cannot convert {:?} to i16", other))),
205        }
206    }
207}
208
209impl FromColumn for i32 {
210    fn from_column(v: Value) -> Result<Self, DbError> {
211        match v {
212            Value::Int(n) => Ok(n as i32),
213            Value::Null => Err(DbError::new("unexpected NULL for i32")),
214            other => Err(DbError::new(format!("cannot convert {:?} to i32", other))),
215        }
216    }
217}
218
219impl FromColumn for i64 {
220    fn from_column(v: Value) -> Result<Self, DbError> {
221        match v {
222            Value::Int(n) => Ok(n),
223            Value::Null => Err(DbError::new("unexpected NULL for i64")),
224            other => Err(DbError::new(format!("cannot convert {:?} to i64", other))),
225        }
226    }
227}
228
229impl FromColumn for u32 {
230    fn from_column(v: Value) -> Result<Self, DbError> {
231        match v {
232            Value::Int(n) => Ok(n as u32),
233            Value::Null => Err(DbError::new("unexpected NULL for u32")),
234            other => Err(DbError::new(format!("cannot convert {:?} to u32", other))),
235        }
236    }
237}
238
239impl FromColumn for u64 {
240    fn from_column(v: Value) -> Result<Self, DbError> {
241        match v {
242            Value::Int(n) => Ok(n as u64),
243            Value::Null => Err(DbError::new("unexpected NULL for u64")),
244            other => Err(DbError::new(format!("cannot convert {:?} to u64", other))),
245        }
246    }
247}
248
249impl FromColumn for f32 {
250    fn from_column(v: Value) -> Result<Self, DbError> {
251        match v {
252            Value::Float(f) => Ok(f as f32),
253            Value::Int(n) => Ok(n as f32),
254            Value::Null => Err(DbError::new("unexpected NULL for f32")),
255            other => Err(DbError::new(format!("cannot convert {:?} to f32", other))),
256        }
257    }
258}
259
260impl FromColumn for f64 {
261    fn from_column(v: Value) -> Result<Self, DbError> {
262        match v {
263            Value::Float(f) => Ok(f),
264            Value::Int(n) => Ok(n as f64),
265            Value::Null => Err(DbError::new("unexpected NULL for f64")),
266            other => Err(DbError::new(format!("cannot convert {:?} to f64", other))),
267        }
268    }
269}
270
271impl FromColumn for bool {
272    fn from_column(v: Value) -> Result<Self, DbError> {
273        match v {
274            Value::Bool(b) => Ok(b),
275            Value::Int(n) => Ok(n != 0),
276            Value::Null => Err(DbError::new("unexpected NULL for bool")),
277            other => Err(DbError::new(format!("cannot convert {:?} to bool", other))),
278        }
279    }
280}
281
282impl FromColumn for String {
283    fn from_column(v: Value) -> Result<Self, DbError> {
284        match v {
285            Value::Text(s) => Ok(s),
286            Value::Null => Err(DbError::new("unexpected NULL for String")),
287            other => Err(DbError::new(format!("cannot convert {:?} to String", other))),
288        }
289    }
290}
291
292/// The value as the driver decoded it (18.30.0).
293impl FromColumn for Value {
294    fn from_column(v: Value) -> Result<Self, DbError> {
295        Ok(v)
296    }
297}
298
299impl<T: FromColumn> FromColumn for Option<T> {
300    fn from_column(v: Value) -> Result<Self, DbError> {
301        match v {
302            Value::Null => Ok(None),
303            other => Ok(Some(T::from_column(other)?)),
304        }
305    }
306}
307
308// ── ToColumn impls ────────────────────────────────────────────────────────────
309
310impl ToColumn for i16 {
311    fn to_column(&self) -> Value { Value::Int(*self as i64) }
312}
313impl ToColumn for i32 {
314    fn to_column(&self) -> Value { Value::Int(*self as i64) }
315}
316impl ToColumn for i64 {
317    fn to_column(&self) -> Value { Value::Int(*self) }
318}
319impl ToColumn for u32 {
320    fn to_column(&self) -> Value { Value::Int(*self as i64) }
321}
322impl ToColumn for u64 {
323    fn to_column(&self) -> Value { Value::Int(*self as i64) }
324}
325impl ToColumn for f32 {
326    fn to_column(&self) -> Value { Value::Float(*self as f64) }
327}
328impl ToColumn for f64 {
329    fn to_column(&self) -> Value { Value::Float(*self) }
330}
331impl ToColumn for bool {
332    fn to_column(&self) -> Value { Value::Bool(*self) }
333}
334impl ToColumn for String {
335    fn to_column(&self) -> Value { Value::Text(self.clone()) }
336}
337impl ToColumn for str {
338    fn to_column(&self) -> Value { Value::Text(self.to_owned()) }
339}
340impl ToColumn for &str {
341    fn to_column(&self) -> Value { Value::Text((*self).to_owned()) }
342}
343impl<T: ToColumn> ToColumn for Option<T> {
344    fn to_column(&self) -> Value {
345        match self {
346            Some(v) => v.to_column(),
347            None => Value::Null,
348        }
349    }
350}
351
352// ── Model trait ───────────────────────────────────────────────────────────────
353
354/// Trait generated by `#[derive(Model)]`. Maps a Rust struct to a database table.
355pub trait Model: Sized + Send + Sync {
356    /// The database table name.
357    fn table_name() -> &'static str;
358
359    /// All column names (excluding `#[ignore]` fields).
360    fn column_names() -> &'static [&'static str];
361
362    /// The primary key column name.
363    fn primary_key_name() -> &'static str;
364
365    /// The primary key value for this instance.
366    fn primary_key_value(&self) -> Value;
367
368    /// Whether the primary key is auto-incremented by the database.
369    fn primary_key_auto_increment() -> bool {
370        false
371    }
372
373    /// Deserialise a database row into this type.
374    fn from_row(row: &ModelRow) -> Result<Self, DbError>;
375
376    /// Serialise this instance's fields as (column, value) pairs.
377    /// Excludes `#[ignore]` fields.
378    fn to_values(&self) -> Vec<(&'static str, Value)>;
379
380    /// The column `save` sets to the time of the insert and never writes
381    /// again (`#[timestamps]`: `created_at`). `None`: no such column.
382    fn created_at_column() -> Option<&'static str> {
383        None
384    }
385
386    /// The column `save` sets to the time of every insert and update
387    /// (`#[timestamps]`: `updated_at`).
388    fn updated_at_column() -> Option<&'static str> {
389        None
390    }
391
392    /// The soft-delete column (`#[soft_delete]`: `deleted_at`): `delete`
393    /// sets it to the time instead of removing the row, and queries leave
394    /// out rows where it isn't `NULL` unless asked for them.
395    fn soft_delete_column() -> Option<&'static str> {
396        None
397    }
398
399    /// The optimistic-locking column (`#[version]`): `1` on insert, one
400    /// more on every update, which only succeeds while the row still has
401    /// the version this copy was read with.
402    fn version_column() -> Option<&'static str> {
403        None
404    }
405}
406
407/// What a [`Model`] type maps to, read from its trait implementation: the
408/// type, its table, its columns and primary key (18.28.0). Rust can't list
409/// the `#[derive(Model)]` types of a program at run time, so whatever
410/// reports models (the dev MCP server's `models` tool,
411/// `DevTools::model::<T>()`) is given each type once.
412///
413/// ```rust,ignore
414/// let info = ModelInfo::of::<User>();
415/// assert_eq!(info.table, "users");
416/// ```
417#[derive(Clone, Debug, PartialEq, Eq)]
418pub struct ModelInfo {
419    /// The Rust type, as [`std::any::type_name`] spells it.
420    pub type_name: &'static str,
421    /// [`Model::table_name`].
422    pub table: &'static str,
423    /// [`Model::column_names`], `#[ignore]` fields left out.
424    pub columns: &'static [&'static str],
425    /// [`Model::primary_key_name`].
426    pub primary_key: &'static str,
427    /// [`Model::primary_key_auto_increment`].
428    pub auto_increment: bool,
429}
430
431impl ModelInfo {
432    /// The mapping of `T`.
433    pub fn of<T: Model>() -> Self {
434        ModelInfo {
435            type_name: std::any::type_name::<T>(),
436            table: T::table_name(),
437            columns: T::column_names(),
438            primary_key: T::primary_key_name(),
439            auto_increment: T::primary_key_auto_increment(),
440        }
441    }
442
443    /// The type's name without its module path (`User` for
444    /// `app::models::User`, `Page` for `app::Page<u8>`).
445    pub fn short_name(&self) -> &'static str {
446        let base = self.type_name.split('<').next().unwrap_or(self.type_name);
447        base.rsplit("::").next().unwrap_or(base)
448    }
449}