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//!
11//! Enable one or more backend features — `model-sqlite`, `model-postgres`,
12//! and `model-mysql` are not mutually exclusive, so a single binary can hold
13//! pools to more than one backend at once (see [`Backend`]):
14//!
15//! ```toml
16//! # SQLite
17//! rust-web-server = { features = ["model-sqlite"] }
18//! # PostgreSQL
19//! rust-web-server = { features = ["model-postgres"] }
20//! # MySQL
21//! rust-web-server = { features = ["model-mysql"] }
22//! # Or more than one at once
23//! rust-web-server = { features = ["model-sqlite", "model-postgres"] }
24//! ```
25
26pub mod backend;
27pub mod connection;
28pub mod migration;
29pub mod pool;
30pub mod query;
31pub mod relation;
32pub mod repository;
33#[cfg(test)]
34mod tests;
35#[cfg(test)]
36mod tls_tests;
37
38pub use backend::Backend;
39pub use connection::{DbConfig, DbSslMode, DbTls};
40pub use migration::MigrationStatus;
41pub use pool::{DbPool, DbTransaction};
42pub use query::{Order, QueryBuilder};
43pub use relation::{BelongsTo, HasMany, HasOne};
44pub use repository::{ModelRepository, Repository};
45pub use crate::pagination::{CursorPage, Page};
46
47// ── Value ─────────────────────────────────────────────────────────────────────
48
49/// Backend-independent SQL value representation.
50#[derive(Debug, Clone, PartialEq)]
51pub enum Value {
52    Null,
53    Bool(bool),
54    Int(i64),
55    Float(f64),
56    Text(String),
57    Bytes(Vec<u8>),
58}
59
60// ── DbError ───────────────────────────────────────────────────────────────────
61
62/// Error type returned by all model/DB operations.
63#[derive(Debug)]
64pub struct DbError(pub String);
65
66impl std::fmt::Display for DbError {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        write!(f, "DbError: {}", self.0)
69    }
70}
71
72impl std::error::Error for DbError {}
73
74impl DbError {
75    pub fn new(msg: impl Into<String>) -> Self {
76        DbError(msg.into())
77    }
78}
79
80// ── ModelRow ──────────────────────────────────────────────────────────────────
81
82/// Backend-independent database row.
83#[derive(Debug, Clone)]
84pub struct ModelRow {
85    #[doc(hidden)]
86    pub columns: Vec<(String, Value)>,
87}
88
89impl ModelRow {
90    pub fn new(columns: Vec<(String, Value)>) -> Self {
91        ModelRow { columns }
92    }
93
94    /// Retrieve and convert a column value by name.
95    pub fn get<T: FromColumn>(&self, col: &str) -> Result<T, DbError> {
96        for (name, val) in &self.columns {
97            if name.eq_ignore_ascii_case(col) {
98                return T::from_column(val.clone());
99            }
100        }
101        Err(DbError::new(format!("column '{}' not found in row", col)))
102    }
103}
104
105// ── FromColumn / ToColumn ─────────────────────────────────────────────────────
106
107/// Convert a `Value` into a Rust type.
108pub trait FromColumn: Sized {
109    fn from_column(v: Value) -> Result<Self, DbError>;
110}
111
112/// Convert a Rust value into a `Value`.
113pub trait ToColumn {
114    fn to_column(&self) -> Value;
115}
116
117// ── FromColumn impls ──────────────────────────────────────────────────────────
118
119impl FromColumn for i16 {
120    fn from_column(v: Value) -> Result<Self, DbError> {
121        match v {
122            Value::Int(n) => Ok(n as i16),
123            Value::Null => Err(DbError::new("unexpected NULL for i16")),
124            other => Err(DbError::new(format!("cannot convert {:?} to i16", other))),
125        }
126    }
127}
128
129impl FromColumn for i32 {
130    fn from_column(v: Value) -> Result<Self, DbError> {
131        match v {
132            Value::Int(n) => Ok(n as i32),
133            Value::Null => Err(DbError::new("unexpected NULL for i32")),
134            other => Err(DbError::new(format!("cannot convert {:?} to i32", other))),
135        }
136    }
137}
138
139impl FromColumn for i64 {
140    fn from_column(v: Value) -> Result<Self, DbError> {
141        match v {
142            Value::Int(n) => Ok(n),
143            Value::Null => Err(DbError::new("unexpected NULL for i64")),
144            other => Err(DbError::new(format!("cannot convert {:?} to i64", other))),
145        }
146    }
147}
148
149impl FromColumn for u32 {
150    fn from_column(v: Value) -> Result<Self, DbError> {
151        match v {
152            Value::Int(n) => Ok(n as u32),
153            Value::Null => Err(DbError::new("unexpected NULL for u32")),
154            other => Err(DbError::new(format!("cannot convert {:?} to u32", other))),
155        }
156    }
157}
158
159impl FromColumn for u64 {
160    fn from_column(v: Value) -> Result<Self, DbError> {
161        match v {
162            Value::Int(n) => Ok(n as u64),
163            Value::Null => Err(DbError::new("unexpected NULL for u64")),
164            other => Err(DbError::new(format!("cannot convert {:?} to u64", other))),
165        }
166    }
167}
168
169impl FromColumn for f32 {
170    fn from_column(v: Value) -> Result<Self, DbError> {
171        match v {
172            Value::Float(f) => Ok(f as f32),
173            Value::Int(n) => Ok(n as f32),
174            Value::Null => Err(DbError::new("unexpected NULL for f32")),
175            other => Err(DbError::new(format!("cannot convert {:?} to f32", other))),
176        }
177    }
178}
179
180impl FromColumn for f64 {
181    fn from_column(v: Value) -> Result<Self, DbError> {
182        match v {
183            Value::Float(f) => Ok(f),
184            Value::Int(n) => Ok(n as f64),
185            Value::Null => Err(DbError::new("unexpected NULL for f64")),
186            other => Err(DbError::new(format!("cannot convert {:?} to f64", other))),
187        }
188    }
189}
190
191impl FromColumn for bool {
192    fn from_column(v: Value) -> Result<Self, DbError> {
193        match v {
194            Value::Bool(b) => Ok(b),
195            Value::Int(n) => Ok(n != 0),
196            Value::Null => Err(DbError::new("unexpected NULL for bool")),
197            other => Err(DbError::new(format!("cannot convert {:?} to bool", other))),
198        }
199    }
200}
201
202impl FromColumn for String {
203    fn from_column(v: Value) -> Result<Self, DbError> {
204        match v {
205            Value::Text(s) => Ok(s),
206            Value::Null => Err(DbError::new("unexpected NULL for String")),
207            other => Err(DbError::new(format!("cannot convert {:?} to String", other))),
208        }
209    }
210}
211
212impl<T: FromColumn> FromColumn for Option<T> {
213    fn from_column(v: Value) -> Result<Self, DbError> {
214        match v {
215            Value::Null => Ok(None),
216            other => Ok(Some(T::from_column(other)?)),
217        }
218    }
219}
220
221// ── ToColumn impls ────────────────────────────────────────────────────────────
222
223impl ToColumn for i16 {
224    fn to_column(&self) -> Value { Value::Int(*self as i64) }
225}
226impl ToColumn for i32 {
227    fn to_column(&self) -> Value { Value::Int(*self as i64) }
228}
229impl ToColumn for i64 {
230    fn to_column(&self) -> Value { Value::Int(*self) }
231}
232impl ToColumn for u32 {
233    fn to_column(&self) -> Value { Value::Int(*self as i64) }
234}
235impl ToColumn for u64 {
236    fn to_column(&self) -> Value { Value::Int(*self as i64) }
237}
238impl ToColumn for f32 {
239    fn to_column(&self) -> Value { Value::Float(*self as f64) }
240}
241impl ToColumn for f64 {
242    fn to_column(&self) -> Value { Value::Float(*self) }
243}
244impl ToColumn for bool {
245    fn to_column(&self) -> Value { Value::Bool(*self) }
246}
247impl ToColumn for String {
248    fn to_column(&self) -> Value { Value::Text(self.clone()) }
249}
250impl ToColumn for str {
251    fn to_column(&self) -> Value { Value::Text(self.to_owned()) }
252}
253impl ToColumn for &str {
254    fn to_column(&self) -> Value { Value::Text((*self).to_owned()) }
255}
256impl<T: ToColumn> ToColumn for Option<T> {
257    fn to_column(&self) -> Value {
258        match self {
259            Some(v) => v.to_column(),
260            None => Value::Null,
261        }
262    }
263}
264
265// ── Model trait ───────────────────────────────────────────────────────────────
266
267/// Trait generated by `#[derive(Model)]`. Maps a Rust struct to a database table.
268pub trait Model: Sized + Send + Sync {
269    /// The database table name.
270    fn table_name() -> &'static str;
271
272    /// All column names (excluding `#[ignore]` fields).
273    fn column_names() -> &'static [&'static str];
274
275    /// The primary key column name.
276    fn primary_key_name() -> &'static str;
277
278    /// The primary key value for this instance.
279    fn primary_key_value(&self) -> Value;
280
281    /// Whether the primary key is auto-incremented by the database.
282    fn primary_key_auto_increment() -> bool {
283        false
284    }
285
286    /// Deserialise a database row into this type.
287    fn from_row(row: &ModelRow) -> Result<Self, DbError>;
288
289    /// Serialise this instance's fields as (column, value) pairs.
290    /// Excludes `#[ignore]` fields.
291    fn to_values(&self) -> Vec<(&'static str, Value)>;
292}
293
294/// What a [`Model`] type maps to, read from its trait implementation: the
295/// type, its table, its columns and primary key (18.28.0). Rust can't list
296/// the `#[derive(Model)]` types of a program at run time, so whatever
297/// reports models (the dev MCP server's `models` tool,
298/// `DevTools::model::<T>()`) is given each type once.
299///
300/// ```rust,ignore
301/// let info = ModelInfo::of::<User>();
302/// assert_eq!(info.table, "users");
303/// ```
304#[derive(Clone, Debug, PartialEq, Eq)]
305pub struct ModelInfo {
306    /// The Rust type, as [`std::any::type_name`] spells it.
307    pub type_name: &'static str,
308    /// [`Model::table_name`].
309    pub table: &'static str,
310    /// [`Model::column_names`], `#[ignore]` fields left out.
311    pub columns: &'static [&'static str],
312    /// [`Model::primary_key_name`].
313    pub primary_key: &'static str,
314    /// [`Model::primary_key_auto_increment`].
315    pub auto_increment: bool,
316}
317
318impl ModelInfo {
319    /// The mapping of `T`.
320    pub fn of<T: Model>() -> Self {
321        ModelInfo {
322            type_name: std::any::type_name::<T>(),
323            table: T::table_name(),
324            columns: T::column_names(),
325            primary_key: T::primary_key_name(),
326            auto_increment: T::primary_key_auto_increment(),
327        }
328    }
329
330    /// The type's name without its module path (`User` for
331    /// `app::models::User`, `Page` for `app::Page<u8>`).
332    pub fn short_name(&self) -> &'static str {
333        let base = self.type_name.split('<').next().unwrap_or(self.type_name);
334        base.rsplit("::").next().unwrap_or(base)
335    }
336}