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