1use std::{
20 ops::{Deref, DerefMut},
21 time::Duration,
22};
23
24pub use sqlx;
25use sqlx::{
26 AssertSqlSafe, PgConnection, PgPool, Postgres, Row,
27 migrate::{MigrateError, Migrator},
28 pool::PoolConnection,
29 postgres::PgPoolOptions,
30};
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct PoolConfig {
35 pub uri: String,
37 pub min_connections: u32,
39 pub max_connections: u32,
41 pub connect_timeout_ms: u64,
43 pub idle_timeout_ms: u64,
45}
46
47impl PoolConfig {
48 #[must_use]
50 pub fn new(uri: impl Into<String>) -> Self {
51 Self {
52 uri: uri.into(),
53 min_connections: 1,
54 max_connections: 10,
55 connect_timeout_ms: 5_000,
56 idle_timeout_ms: 600_000,
57 }
58 }
59
60 #[must_use]
62 pub fn options(&self) -> PgPoolOptions {
63 PgPoolOptions::new()
64 .min_connections(self.min_connections)
65 .max_connections(self.max_connections)
66 .acquire_timeout(Duration::from_millis(self.connect_timeout_ms))
67 .idle_timeout(Duration::from_millis(self.idle_timeout_ms))
68 }
69}
70
71pub async fn connect(config: &PoolConfig) -> Result<PgPool, sqlx::Error> {
77 config.options().connect(&config.uri).await
78}
79
80pub async fn ping(pool: &PgPool) -> Result<(), sqlx::Error> {
86 sqlx::query("SELECT 1").execute(pool).await?;
87 Ok(())
88}
89
90pub enum Db<'a> {
107 Pool(&'a PgPool),
109 Conn(&'a mut PgConnection),
111}
112
113impl<'a> From<&'a PgPool> for Db<'a> {
114 fn from(pool: &'a PgPool) -> Self {
115 Self::Pool(pool)
116 }
117}
118
119impl<'a> From<&'a mut PgConnection> for Db<'a> {
120 fn from(conn: &'a mut PgConnection) -> Self {
121 Self::Conn(conn)
122 }
123}
124
125impl<'a> Db<'a> {
126 pub async fn acquire(self) -> Result<Conn<'a>, sqlx::Error> {
132 match self {
133 Self::Pool(pool) => Ok(Conn::Pooled(pool.acquire().await?)),
134 Self::Conn(conn) => Ok(Conn::Borrowed(conn)),
135 }
136 }
137}
138
139pub enum Conn<'a> {
141 Pooled(PoolConnection<Postgres>),
143 Borrowed(&'a mut PgConnection),
145}
146
147impl Deref for Conn<'_> {
148 type Target = PgConnection;
149
150 fn deref(&self) -> &PgConnection {
151 match self {
152 Self::Pooled(conn) => conn,
153 Self::Borrowed(conn) => conn,
154 }
155 }
156}
157
158impl DerefMut for Conn<'_> {
159 fn deref_mut(&mut self) -> &mut PgConnection {
160 match self {
161 Self::Pooled(conn) => conn,
162 Self::Borrowed(conn) => conn,
163 }
164 }
165}
166
167#[must_use]
170pub fn is_unique_violation(error: &sqlx::Error) -> bool {
171 error
172 .as_database_error()
173 .is_some_and(sqlx::error::DatabaseError::is_unique_violation)
174}
175
176#[must_use]
188pub fn quote_identifier(name: &str) -> Option<String> {
189 let mut chars = name.chars();
190 let first = chars.next()?;
191 let valid = (first.is_ascii_lowercase() || first == '_')
192 && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
193 valid.then(|| format!("\"{name}\""))
194}
195
196fn quote_all<S: AsRef<str>>(names: &[S]) -> Result<Vec<String>, sqlx::Error> {
198 names
199 .iter()
200 .map(|name| {
201 quote_identifier(name.as_ref()).ok_or_else(|| {
202 sqlx::Error::Protocol(format!("invalid identifier: {}", name.as_ref()))
203 })
204 })
205 .collect()
206}
207
208pub async fn migrate(pool: &PgPool, migrator: &Migrator) -> Result<(), MigrateError> {
215 migrator.run(pool).await
216}
217
218async fn applied_versions(pool: &PgPool) -> Result<Vec<i64>, sqlx::Error> {
221 let exists: bool = sqlx::query("SELECT to_regclass('_sqlx_migrations') IS NOT NULL AS found")
222 .fetch_one(pool)
223 .await?
224 .try_get("found")?;
225 if !exists {
226 return Ok(Vec::new());
227 }
228 let rows =
229 sqlx::query("SELECT version FROM _sqlx_migrations WHERE success ORDER BY version DESC")
230 .fetch_all(pool)
231 .await?;
232 rows.iter().map(|row| row.try_get("version")).collect()
233}
234
235pub async fn revert(pool: &PgPool, migrator: &Migrator, steps: u32) -> Result<(), MigrateError> {
241 let applied = applied_versions(pool).await?;
242 let steps = usize::try_from(steps).unwrap_or(usize::MAX);
243 let target = applied.get(steps).copied().unwrap_or(0);
246 migrator.undo(pool, target).await
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct MigrationStatus {
252 pub version: i64,
254 pub description: String,
256 pub applied: bool,
258}
259
260pub async fn status(
266 pool: &PgPool,
267 migrator: &Migrator,
268) -> Result<Vec<MigrationStatus>, sqlx::Error> {
269 let applied = applied_versions(pool).await?;
270 Ok(migrator
271 .iter()
272 .filter(|migration| !migration.migration_type.is_down_migration())
273 .map(|migration| MigrationStatus {
274 version: migration.version,
275 description: migration.description.to_string(),
276 applied: applied.contains(&migration.version),
277 })
278 .collect())
279}
280
281pub async fn truncate<S: AsRef<str>>(pool: &PgPool, tables: &[S]) -> Result<(), sqlx::Error> {
288 if tables.is_empty() {
289 return Ok(());
290 }
291 let tables = quote_all(tables)?.join(", ");
292 let sql = format!("TRUNCATE {tables} RESTART IDENTITY CASCADE");
293 sqlx::query(AssertSqlSafe(sql)).execute(pool).await?;
294 Ok(())
295}
296
297pub async fn dangerously_recreate(pool: &PgPool, migrator: &Migrator) -> Result<(), MigrateError> {
312 let tables: Vec<String> =
313 sqlx::query("SELECT tablename::text AS name FROM pg_tables WHERE schemaname = 'public'")
314 .fetch_all(pool)
315 .await?
316 .iter()
317 .map(|row| row.try_get("name"))
318 .collect::<Result<_, _>>()?;
319 let types: Vec<String> = sqlx::query(
321 "SELECT t.typname::text AS name FROM pg_type t
322 JOIN pg_namespace n ON n.oid = t.typnamespace
323 WHERE t.typtype = 'e' AND n.nspname = 'public'
324 AND NOT EXISTS (
325 SELECT 1 FROM pg_depend d WHERE d.objid = t.oid AND d.deptype = 'e'
326 )",
327 )
328 .fetch_all(pool)
329 .await?
330 .iter()
331 .map(|row| row.try_get("name"))
332 .collect::<Result<_, _>>()?;
333
334 if !tables.is_empty() {
335 let names = quote_all(&tables)?.join(", ");
336 let sql = format!("DROP TABLE {names} CASCADE");
337 sqlx::query(AssertSqlSafe(sql)).execute(pool).await?;
338 }
339 for name in quote_all(&types)? {
340 let sql = format!("DROP TYPE {name} CASCADE");
341 sqlx::query(AssertSqlSafe(sql)).execute(pool).await?;
342 }
343 migrate(pool, migrator).await
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn identifiers_are_quoted() {
352 assert_eq!(quote_identifier("users").as_deref(), Some("\"users\""));
353 assert_eq!(quote_identifier("_a1").as_deref(), Some("\"_a1\""));
354 }
355
356 #[test]
357 fn unsafe_identifiers_are_refused() {
358 for name in [
359 "", "1users", "Users", "a b", "a\"b", "a;b", "a.b", "users--", "é",
360 ] {
361 assert_eq!(quote_identifier(name), None, "{name:?}");
362 }
363 }
364
365 #[test]
366 fn quote_all_fails_on_any_bad_name() {
367 assert!(quote_all(&["good", "also_good"]).is_ok());
368 assert!(quote_all(&["good", "bad name"]).is_err());
369 }
370
371 #[test]
372 fn pool_settings_reach_the_options() {
373 let config = PoolConfig {
374 min_connections: 2,
375 max_connections: 7,
376 connect_timeout_ms: 1_500,
377 idle_timeout_ms: 9_000,
378 ..PoolConfig::new("postgres://x")
379 };
380 let options = config.options();
381 assert_eq!(options.get_min_connections(), 2);
382 assert_eq!(options.get_max_connections(), 7);
383 assert_eq!(options.get_acquire_timeout(), Duration::from_millis(1_500));
384 assert_eq!(options.get_idle_timeout(), Some(Duration::from_secs(9)));
385 }
386
387 #[test]
388 fn errors_that_are_not_from_the_database_are_not_unique_violations() {
389 assert!(!is_unique_violation(&sqlx::Error::RowNotFound));
390 }
391}