1use crate::config::ConfigError;
2use cfg_if::cfg_if;
3use sqlx::migrate::MigrateError;
4use sqlx::pool::PoolConnection;
5use sqlx::{PgPool, Postgres};
6use thiserror::Error;
7cfg_if! {
8 if #[cfg(test)] {
9 use std::cell::RefCell;
10 } else if #[cfg(feature = "test-utils")] {
11 use std::cell::RefCell;
12 use sqlx::postgres::PgPoolOptions;
13 use std::env::var;
14 use std::sync::LazyLock;
15 use std::time::Duration;
16 use tokio::sync::OnceCell;
17
18 static DB_URL: LazyLock<Option<String>> = LazyLock::new(|| var("DATABASE_URL").ok());
19 } else {
20 use sqlx::postgres::PgPoolOptions;
21 use std::env::var;
22 use std::sync::LazyLock;
23 use std::time::Duration;
24 use tokio::sync::OnceCell;
25
26 static DB_URL: LazyLock<Option<String>> = LazyLock::new(|| var("DATABASE_URL").ok());
27 }
28}
29
30#[derive(Debug, Error)]
31pub enum DBError {
32 #[error("Database error: {0}")]
33 Sqlx(#[from] sqlx::Error),
34 #[error("DB migration error: {0}")]
35 Migration(#[from] MigrateError),
36 #[error("Configuration access error")]
37 Config(#[from] ConfigError),
38 #[error("DATABASE_URL is not provided")]
39 MissingUrl,
40 #[error("The database role lacks CREATEDB privilege")]
41 NoCreateDb,
42 #[error("Failed to generate an authentication keypair")]
43 KeyGen,
44 #[error("No connection-pool slot became free in time")]
45 PoolBudgetExhausted,
46 #[error("This account is undergoing maintenance and is temporarily unavailable")]
47 TenantQuarantined,
48 #[error("This account is disabled")]
49 TenantDisabled,
50 #[error("A fleet migration worker did not finish: {0}")]
51 FleetWorkerLost(String),
52 #[error("{0}")]
53 GlobalSchemaStale(String),
54 #[error("Refusing to build a connection string from database name {0:?}")]
55 UnsafeDatabaseName(String),
56 #[cfg(test)]
57 #[error("DB_POOL must be set; see local_db_sqlx_test macro")]
58 TestPoolUnset,
59}
60
61#[cfg(not(test))]
71pub(crate) const ADMIN_MAX_CONNECTIONS: u32 = 5;
72
73#[cfg(any(test, feature = "test-utils"))]
77pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../migrations");
78
79pub async fn migrate_db() -> Result<(), DBError> {
80 let pool = get_pool().await?;
81 Ok(sqlx::migrate!("../migrations").run(&pool).await?)
82}
83
84pub async fn get_connection() -> Result<PoolConnection<Postgres>, DBError> {
85 Ok(get_pool().await?.acquire().await?)
86}
87
88pub async fn execute_raw(sql: &str) -> Result<(), DBError> {
93 let pool = get_pool().await?;
94 sqlx::raw_sql(sqlx::AssertSqlSafe(sql))
95 .execute(&pool)
96 .await?;
97 Ok(())
98}
99
100cfg_if! {
101 if #[cfg(test)] {
102 thread_local!(pub static DB_POOL: RefCell<Option<PgPool>> = const {
112 RefCell::new(None)
113 });
114
115 #[must_use]
119 pub fn test_pool_is_set() -> bool {
120 DB_POOL.with_borrow(Option::is_some)
121 }
122
123 async fn get_pool() -> Result<PgPool, DBError> {
124 DB_POOL
125 .with_borrow(Clone::clone)
126 .ok_or(DBError::TestPoolUnset)
127 }
128
129 } else if #[cfg(feature = "test-utils")] {
130 thread_local!(pub static DB_POOL: RefCell<Option<PgPool>> = const {
140 RefCell::new(None)
141 });
142
143 #[must_use]
147 pub fn test_pool_is_set() -> bool {
148 DB_POOL.with_borrow(Option::is_some)
149 }
150
151 static FALLBACK_POOL: OnceCell<PgPool> = OnceCell::const_new();
152
153 async fn get_pool() -> Result<PgPool, DBError> {
154 if let Some(pool) = DB_POOL.with_borrow(Clone::clone) {
155 return Ok(pool);
156 }
157 let url = DB_URL.as_deref().ok_or(DBError::MissingUrl)?;
158 FALLBACK_POOL
159 .get_or_try_init(|| async {
160 log::debug!("Fallback pool initialization");
161 let options = PgPoolOptions::new()
162 .max_connections(ADMIN_MAX_CONNECTIONS)
163 .acquire_timeout(Duration::from_secs(10));
164 Ok(options.connect(url).await?)
165 })
166 .await
167 .cloned()
168 }
169
170 } else {
171 static DB_POOL: OnceCell<PgPool> = OnceCell::const_new();
173
174 async fn get_pool() -> Result<PgPool, DBError> {
175 let url = DB_URL.as_deref().ok_or(DBError::MissingUrl)?;
176 DB_POOL
177 .get_or_try_init(|| async {
178 log::debug!("Pool initialization");
179 let options = PgPoolOptions::new()
180 .max_connections(ADMIN_MAX_CONNECTIONS)
181 .acquire_timeout(Duration::from_secs(10));
182 Ok(options.connect(url).await?)
183 })
184 .await
185 .cloned()
186 }
187 }
188}
189
190pub fn admin_database_url() -> Result<String, DBError> {
200 std::env::var("DATABASE_URL").map_err(|_| DBError::MissingUrl)
201}
202
203pub async fn migrate_on(conn: &mut sqlx::PgConnection) -> Result<(), DBError> {
213 sqlx::migrate!("../migrations")
214 .run(conn)
215 .await
216 .map_err(DBError::from)
217}
218
219pub async fn migrate_url(url: &str) -> Result<(), DBError> {
227 let pool = sqlx::postgres::PgPoolOptions::new()
228 .max_connections(1)
229 .acquire_timeout(std::time::Duration::from_secs(10))
230 .connect(url)
231 .await?;
232 let result = sqlx::migrate!("../migrations").run(&pool).await;
233 pool.close().await;
237 result.map_err(DBError::from)
238}
239
240#[cfg(test)]
241mod db_tests;