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