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::Cell;
10 } else if #[cfg(feature = "test-utils")] {
11 use std::cell::Cell;
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}
45
46#[cfg(any(test, feature = "test-utils"))]
50pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../migrations");
51
52pub async fn migrate_db() -> Result<(), DBError> {
53 Ok(sqlx::migrate!("../migrations")
54 .run(get_pool().await?)
55 .await?)
56}
57
58pub async fn get_connection() -> Result<PoolConnection<Postgres>, DBError> {
59 Ok(get_pool().await?.acquire().await?)
60}
61
62pub async fn execute_raw(sql: &str) -> Result<(), DBError> {
67 sqlx::raw_sql(sqlx::AssertSqlSafe(sql))
68 .execute(get_pool().await?)
69 .await?;
70 Ok(())
71}
72
73cfg_if! {
74 if #[cfg(test)] {
75 thread_local!(pub static DB_POOL: Cell<*const PgPool> = const {
81 Cell::new(std::ptr::null())
82 });
83
84 #[must_use]
88 pub fn test_pool_is_set() -> bool {
89 DB_POOL.with(|c| !c.get().is_null())
90 }
91
92 async fn get_pool() -> Result<&'static PgPool, DBError> {
93 let p = DB_POOL.with(|c| c.get());
94 assert!(!p.is_null(), "DB_POOL must be set; see local_db_sqlx_test macro");
95 unsafe { Ok(&*p) }
96 }
97
98 } else if #[cfg(feature = "test-utils")] {
99 thread_local!(pub static DB_POOL: Cell<*const PgPool> = const {
107 Cell::new(std::ptr::null())
108 });
109
110 #[must_use]
114 pub fn test_pool_is_set() -> bool {
115 DB_POOL.with(|c| !c.get().is_null())
116 }
117
118 static FALLBACK_POOL: OnceCell<PgPool> = OnceCell::const_new();
119
120 async fn get_pool() -> Result<&'static PgPool, DBError> {
121 let p = DB_POOL.with(|c| c.get());
122 if !p.is_null() {
123 return unsafe { Ok(&*p) };
124 }
125 let url = DB_URL.as_deref().ok_or(DBError::MissingUrl)?;
126 FALLBACK_POOL
127 .get_or_try_init(|| async {
128 log::debug!("Fallback pool initialization");
129 let options = PgPoolOptions::new()
130 .max_connections(10)
131 .acquire_timeout(Duration::from_secs(10));
132 Ok(options.connect(url).await?)
133 })
134 .await
135 }
136
137 } else {
138 static DB_POOL: OnceCell<PgPool> = OnceCell::const_new();
140
141 async fn get_pool() -> Result<&'static PgPool, DBError> {
142 let url = DB_URL.as_deref().ok_or(DBError::MissingUrl)?;
143 DB_POOL
144 .get_or_try_init(|| async {
145 log::debug!("Pool initialization");
146 let options = PgPoolOptions::new()
147 .max_connections(10)
148 .acquire_timeout(Duration::from_secs(10));
149 Ok(options.connect(url).await?)
150 })
151 .await
152 }
153 }
154}
155
156pub fn admin_database_url() -> Result<String, DBError> {
169 std::env::var("DATABASE_URL").map_err(|_| DBError::MissingUrl)
170}
171
172pub async fn migrate_url(url: &str) -> Result<(), DBError> {
180 let pool = sqlx::postgres::PgPoolOptions::new()
181 .max_connections(1)
182 .acquire_timeout(std::time::Duration::from_secs(10))
183 .connect(url)
184 .await?;
185 let result = sqlx::migrate!("../migrations").run(&pool).await;
186 pool.close().await;
190 result.map_err(DBError::from)
191}
192
193#[cfg(test)]
194mod db_tests {
195 use sqlx::PgPool;
196 use tokio::sync::OnceCell;
197
198 static CONTEXT: OnceCell<()> = OnceCell::const_new();
200
201 async fn setup() {
202 CONTEXT
203 .get_or_init(|| async {
204 #[cfg(feature = "testlog")]
205 let _ = env_logger::builder()
206 .is_test(true)
207 .filter_level(log::LevelFilter::Trace)
208 .try_init();
209 })
210 .await;
211 }
212
213 #[sqlx::test(migrator = "server::db::MIGRATOR")]
214 async fn migrations_create_schema_without_seed(pool: PgPool) -> sqlx::Result<()> {
215 setup().await;
216
217 let mut conn = pool.acquire().await?;
218
219 let config_rows: i64 = sqlx::query_scalar("SELECT count(*) FROM config")
223 .fetch_one(&mut *conn)
224 .await?;
225 assert_eq!(config_rows, 0, "migration set must not seed config rows");
226
227 Ok(())
228 }
229}