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