Skip to main content

rustlavel_db/
lib.rs

1//! rustlavel-db: the database package.
2//!
3//! A PostgreSQL driver written directly on the version 3 wire protocol, a
4//! connection pool, a query builder, schema migrations, and seeding. Enabled
5//! with `cargo add rustlavel-db` — an application that never adds it never
6//! compiles a line of this.
7
8pub mod base64;
9pub mod builder;
10pub mod config;
11pub mod connections;
12pub mod credentials;
13pub mod dialect;
14pub mod driver;
15pub mod migration;
16pub mod model;
17pub mod mysql;
18pub mod pagination;
19pub mod pool;
20pub mod postgres;
21pub mod random;
22pub mod row;
23pub mod schema;
24pub mod sqlserver;
25pub mod tls;
26pub mod value;
27
28pub use builder::{Direction, QueryBuilder};
29pub use config::DatabaseConfig;
30pub use dialect::{ColumnType, Dialect, ReturningStyle};
31pub use driver::{Driver, DriverConnection, QueryResult};
32pub use connections::{Connections, DEFAULT_BUDGET};
33pub use migration::{Faker, Migration, Migrator, Seeder};
34pub use model::{Model, ModelExt, belongs_to, has_many};
35pub use mysql::{MySqlConnection, MySqlDriver};
36pub use pagination::{CursorPage, Page};
37pub use postgres::connection::{log_bindings, set_log_bindings};
38pub use pool::{Pool, PooledConnection};
39pub use schema::{Schema, Table};
40pub use sqlserver::{SqlServerConnection, SqlServerDriver};
41pub use row::{Row, rows_to_json};
42pub use value::{FromValue, Value};
43
44pub use rustlavel_core::{Error, Result};
45use std::sync::Arc;
46
47/// Build the driver a configuration asks for.
48///
49/// A driver that is not compiled into this build says so by name, rather than
50/// failing later with something about a connection.
51fn driver_for(config: DatabaseConfig) -> Result<Arc<dyn Driver>> {
52    match config.driver.as_str() {
53        "postgres" => Ok(Arc::new(postgres::PostgresDriver::new(config))),
54        "mysql" => Ok(Arc::new(mysql::MySqlDriver::new(config))),
55        "sqlserver" => Ok(Arc::new(sqlserver::SqlServerDriver::new(config))),
56        other => Err(Error::msg(format!(
57            "the `{other}` driver is not available in this build. \
58             Point DATABASE_URL at a database this build supports."
59        ))),
60    }
61}
62
63/// `#[derive(Model)]`.
64pub use rustlavel_macros::Model;
65
66/// What a migration, seeder, or model file imports.
67pub mod prelude {
68    pub use crate::connections::Connections;
69    pub use crate::migration::{Faker, Migrator, Seeder};
70    pub use crate::model::{ModelExt, belongs_to, has_many};
71    pub use crate::schema::{Schema, Table};
72    pub use crate::{CursorPage, Database, Model, Page, QueryBuilder, Row, Value};
73    pub use rustlavel_core::{Error, Json, Result};
74}
75
76/// The application's handle on the database.
77///
78/// Registered as application state, so a handler reaches it with
79/// `req.state::<Database>()`.
80#[derive(Clone)]
81pub struct Database {
82    pool: Pool,
83    dialect: Arc<dyn Dialect>,
84}
85
86impl Database {
87    /// Connect using a URL: `postgres://user:password@host:port/database`.
88    pub async fn connect(url: &str) -> Result<Database> {
89        Database::with_config(DatabaseConfig::from_url(url)?).await
90    }
91
92    /// Connect using explicit settings, verifying the connection works.
93    pub async fn with_config(config: DatabaseConfig) -> Result<Database> {
94        let database = Database::lazy(config)?;
95        database.pool.verify().await?;
96        Ok(database)
97    }
98
99    /// Build a handle without touching the network. Useful when the process
100    /// should start even if the database is briefly down.
101    pub fn lazy(config: DatabaseConfig) -> Result<Database> {
102        Ok(Database::with_driver(driver_for(config)?))
103    }
104
105    /// Use a driver directly — how a database this crate does not know about
106    /// would be plugged in.
107    pub fn with_driver(driver: Arc<dyn Driver>) -> Database {
108        let dialect = driver.dialect();
109        Database { pool: Pool::new(driver), dialect }
110    }
111
112    /// What SQL this connection speaks.
113    ///
114    /// The query and schema builders take it, which is how one builder produces
115    /// correct SQL for three different databases.
116    pub fn dialect(&self) -> &dyn Dialect {
117        self.dialect.as_ref()
118    }
119
120    pub fn pool(&self) -> &Pool {
121        &self.pool
122    }
123
124    /// Start a query: `db.table("users").filter("active", true).get(&db).await`.
125    pub fn table(&self, name: &str) -> QueryBuilder {
126        QueryBuilder::new(name)
127    }
128
129    /// Run a query and return every row.
130    pub async fn select(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>> {
131        let mut connection = self.pool.acquire().await?;
132        Ok(connection.query(sql, params).await?.rows)
133    }
134
135    /// Run a query expecting at most one row.
136    pub async fn select_one(&self, sql: &str, params: &[Value]) -> Result<Option<Row>> {
137        Ok(self.select(sql, params).await?.into_iter().next())
138    }
139
140    /// Run a statement and return the number of rows it affected.
141    pub async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64> {
142        let mut connection = self.pool.acquire().await?;
143        Ok(connection.query(sql, params).await?.affected)
144    }
145
146    /// Run one or more statements with no parameters — DDL, mostly.
147    pub async fn run(&self, sql: &str) -> Result<u64> {
148        let mut connection = self.pool.acquire().await?;
149        Ok(connection.simple_query(sql).await?.affected)
150    }
151
152    /// Run an insert and hand back the key the database generated.
153    ///
154    /// Three mechanisms, one method: PostgreSQL returns a row from `RETURNING`,
155    /// SQL Server from `OUTPUT`, and MySQL reports the id in the packet that
156    /// acknowledges the insert, with no row at all.
157    pub async fn insert_returning_key(
158        &self,
159        sql: &str,
160        params: &[Value],
161        column: &str,
162    ) -> Result<Option<Value>> {
163        let mut connection = self.pool.acquire().await?;
164        let result = connection.query(sql, params).await?;
165
166        if let Some(row) = result.rows.first() {
167            // Named lookup where the database labelled the column, positional
168            // where it did not.
169            return Ok(Some(row.value(column).or_else(|_| row.value_at(0))?.clone()));
170        }
171        Ok(result.last_insert_id.map(Value::Int))
172    }
173
174    /// Read a single value from the first column of the first row.
175    pub async fn scalar<T: FromValue>(&self, sql: &str, params: &[Value]) -> Result<Option<T>> {
176        match self.select_one(sql, params).await? {
177            Some(row) => row.get_at::<T>(0).map(Some),
178            None => Ok(None),
179        }
180    }
181
182    /// Begin a transaction.
183    ///
184    /// Rust's borrow rules make Laravel's `DB::transaction(closure)` shape
185    /// awkward — the closure's future would have to borrow the connection it
186    /// was handed — so the transaction is a value you hold instead:
187    ///
188    /// ```ignore
189    /// let mut tx = db.begin().await?;
190    /// tx.execute("update accounts set balance = balance - $1", &[amount]).await?;
191    /// tx.commit().await?;
192    /// ```
193    ///
194    /// Dropping it without committing rolls back, so an early `?` cannot leave
195    /// a half-finished transaction behind.
196    pub async fn begin(&self) -> Result<Transaction> {
197        let mut connection = self.pool.acquire().await?;
198        connection.simple_query(self.dialect.begin_sql()).await?;
199        Ok(Transaction {
200            connection: Some(connection),
201            dialect: Arc::clone(&self.dialect),
202            finished: false,
203        })
204    }
205
206    /// Close every pooled connection.
207    pub async fn close(&self) {
208        self.pool.close().await;
209    }
210}
211
212/// An open transaction, holding its connection until it ends.
213pub struct Transaction {
214    connection: Option<PooledConnection>,
215    /// Kept so committing and rolling back use this database's own words.
216    dialect: Arc<dyn Dialect>,
217    finished: bool,
218}
219
220impl Transaction {
221    fn connection(&mut self) -> Result<&mut PooledConnection> {
222        self.connection
223            .as_mut()
224            .ok_or_else(|| Error::msg("this transaction has already finished"))
225    }
226
227    /// The dialect this transaction speaks, so a query builder can render for
228    /// it. See the `*_in` methods on [`QueryBuilder`].
229    pub fn dialect(&self) -> &dyn Dialect {
230        self.dialect.as_ref()
231    }
232
233    pub async fn select(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>> {
234        Ok(self.connection()?.query(sql, params).await?.rows)
235    }
236
237    pub async fn select_one(&mut self, sql: &str, params: &[Value]) -> Result<Option<Row>> {
238        Ok(self.select(sql, params).await?.into_iter().next())
239    }
240
241    pub async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64> {
242        Ok(self.connection()?.query(sql, params).await?.affected)
243    }
244
245    pub async fn run(&mut self, sql: &str) -> Result<u64> {
246        Ok(self.connection()?.simple_query(sql).await?.affected)
247    }
248
249    pub async fn scalar<T: FromValue>(&mut self, sql: &str, params: &[Value]) -> Result<Option<T>> {
250        match self.select_one(sql, params).await? {
251            Some(row) => row.get_at::<T>(0).map(Some),
252            None => Ok(None),
253        }
254    }
255
256    /// A named savepoint, so part of a transaction can be undone on its own.
257    pub async fn savepoint(&mut self, name: &str) -> Result<()> {
258        validate_identifier(name)?;
259        let sql = self.dialect.savepoint_sql(name);
260        self.connection()?.simple_query(&sql).await?;
261        Ok(())
262    }
263
264    pub async fn rollback_to(&mut self, name: &str) -> Result<()> {
265        validate_identifier(name)?;
266        let sql = self.dialect.rollback_to_savepoint_sql(name);
267        self.connection()?.simple_query(&sql).await?;
268        Ok(())
269    }
270
271    /// Commit and release the connection.
272    pub async fn commit(mut self) -> Result<()> {
273        let sql = self.dialect.commit_sql();
274        self.connection()?.simple_query(sql).await?;
275        self.finished = true;
276        Ok(())
277    }
278
279    /// Roll back and release the connection.
280    pub async fn rollback(mut self) -> Result<()> {
281        let sql = self.dialect.rollback_sql();
282        self.connection()?.simple_query(sql).await?;
283        self.finished = true;
284        Ok(())
285    }
286}
287
288impl Drop for Transaction {
289    fn drop(&mut self) {
290        if self.finished {
291            return;
292        }
293        // Nothing committed it, so undo it. The pool would discard a connection
294        // left in a transaction anyway; rolling back explicitly returns it to
295        // service instead of throwing it away.
296        if let Some(mut connection) = self.connection.take() {
297            let sql = self.dialect.rollback_sql();
298            tokio::spawn(async move {
299                let _ = connection.simple_query(sql).await;
300            });
301        }
302    }
303}
304
305/// Reject anything that is not a plain identifier.
306///
307/// Identifiers cannot be sent as parameters, so every place the framework
308/// interpolates one into SQL passes through here first.
309pub fn validate_identifier(name: &str) -> Result<()> {
310    let valid = !name.is_empty()
311        && name.len() <= 63
312        && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
313        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
314
315    if valid {
316        Ok(())
317    } else {
318        Err(Error::msg(format!(
319            "`{name}` is not a valid SQL identifier. Identifiers may contain letters, digits and \
320             underscores, and must not start with a digit."
321        )))
322    }
323}
324
325/// Quote an identifier after validating it.
326pub fn quote_identifier(name: &str) -> Result<String> {
327    // A qualified name (`schema.table`) is validated one part at a time.
328    let quoted: Result<Vec<String>> = name
329        .split('.')
330        .map(|part| validate_identifier(part).map(|_| format!("\"{part}\"")))
331        .collect();
332    Ok(quoted?.join("."))
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn accepts_ordinary_identifiers() {
341        for name in ["users", "user_profiles", "_private", "t1"] {
342            assert!(validate_identifier(name).is_ok(), "{name} should be valid");
343        }
344    }
345
346    #[test]
347    fn rejects_anything_that_could_alter_a_statement() {
348        for name in ["users; drop table users", "user\"s", "1abc", "", "a b", "users--"] {
349            assert!(validate_identifier(name).is_err(), "{name:?} should be rejected");
350        }
351    }
352
353    #[test]
354    fn quotes_qualified_names_part_by_part() {
355        assert_eq!(quote_identifier("public.users").unwrap(), "\"public\".\"users\"");
356        assert!(quote_identifier("public.users; drop table x").is_err());
357    }
358}