Lines
93.44 %
Functions
64.52 %
Branches
100 %
use crate::config::ConfigError;
use cfg_if::cfg_if;
use sqlx::migrate::MigrateError;
use sqlx::pool::PoolConnection;
use sqlx::{PgPool, Postgres};
use thiserror::Error;
cfg_if! {
if #[cfg(test)] {
use std::cell::RefCell;
} else if #[cfg(feature = "test-utils")] {
use sqlx::postgres::PgPoolOptions;
use std::env::var;
use std::sync::LazyLock;
use std::time::Duration;
use tokio::sync::OnceCell;
static DB_URL: LazyLock<Option<String>> = LazyLock::new(|| var("DATABASE_URL").ok());
} else {
}
#[derive(Debug, Error)]
pub enum DBError {
#[error("Database error: {0}")]
Sqlx(#[from] sqlx::Error),
#[error("DB migration error: {0}")]
Migration(#[from] MigrateError),
#[error("Configuration access error")]
Config(#[from] ConfigError),
#[error("DATABASE_URL is not provided")]
MissingUrl,
#[error("The database role lacks CREATEDB privilege")]
NoCreateDb,
#[error("Failed to generate an authentication keypair")]
KeyGen,
#[error("No connection-pool slot became free in time")]
PoolBudgetExhausted,
#[error("This account is undergoing maintenance and is temporarily unavailable")]
TenantQuarantined,
#[error("This account is disabled")]
TenantDisabled,
#[error("A fleet migration worker did not finish: {0}")]
FleetWorkerLost(String),
#[error("{0}")]
GlobalSchemaStale(String),
#[error("Refusing to build a connection string from database name {0:?}")]
UnsafeDatabaseName(String),
#[cfg(test)]
#[error("DB_POOL must be set; see local_db_sqlx_test macro")]
TestPoolUnset,
/// Connections the global/admin pool may open, per process.
///
/// Read on every authenticated request (`jwt_auth` re-reads the user row), so
/// this is the web tier's effective concurrency ceiling, not a background
/// allowance. Budget per process is this plus the tenant registry's own
/// ceiling; see ADR 0032.
/// Server's own `cfg(test)` arm asserts an injected `sqlx::test` pool and never
/// builds one, so the constant does not exist there.
#[cfg(not(test))]
pub(crate) const ADMIN_MAX_CONNECTIONS: u32 = 5;
/// Shared migration set. Exposed to tests (server's own and `tests-integration`)
/// so every `#[sqlx::test(migrator = "...")]` site names this once instead of
/// embedding all migrations per test.
#[cfg(any(test, feature = "test-utils"))]
pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../migrations");
pub async fn migrate_db() -> Result<(), DBError> {
let pool = get_pool().await?;
Ok(sqlx::migrate!("../migrations").run(&pool).await?)
pub async fn get_connection() -> Result<PoolConnection<Postgres>, DBError> {
Ok(get_pool().await?.acquire().await?)
/// Runs a multi-statement SQL script against the admin pool via the simple
/// query protocol. Executed against the shared pool (not a
/// `&mut PgConnection`) so the future stays `Send`/spawnable — the `&mut`
/// executor form trips a higher-ranked-lifetime bound once `boot()` is spawned.
pub async fn execute_raw(sql: &str) -> Result<(), DBError> {
sqlx::raw_sql(sqlx::AssertSqlSafe(sql))
.execute(&pool)
.await?;
Ok(())
// Server's own tests rely on `local_db_sqlx_test` (supp_macro) injecting
// a sqlx::test pool into DB_POOL before commands run. Forgetting that
// setup is a programming error — panic loudly rather than silently
// sharing a real DATABASE_URL pool across tests, which would break the
// per-test isolation sqlx::test guarantees.
//
// The slot owns a pool handle rather than pointing at the test's own, so a
// task still running after the test body sees a closed pool instead of
// freed memory.
thread_local!(pub static DB_POOL: RefCell<Option<PgPool>> = const {
RefCell::new(None)
});
/// True when the current thread has installed a test pool. Server-side
/// callers consult this to decide between the test pool and the
/// per-user production pool.
#[must_use]
pub fn test_pool_is_set() -> bool {
DB_POOL.with_borrow(Option::is_some)
async fn get_pool() -> Result<PgPool, DBError> {
DB_POOL
.with_borrow(Clone::clone)
.ok_or(DBError::TestPoolUnset)
// Downstream consumers (the workspace `tests-integration` crate, and
// any web/sshd test that wants to drive a `server::command::*` flow
// against an isolated DB) install a sqlx::test pool via DB_POOL. When
// it is not installed we fall back to the production `DATABASE_URL`
// pool — that's the path web's existing tests take when compiled with
// `--all-features`, since they never set DB_POOL and instead let the
// production pool resolve via JWT'd handlers.
// The slot owns a pool handle for the same reason as the `cfg(test)` arm.
/// True when the current thread has installed a test pool. Used by
/// `User::get_connection` to choose between the test override and the
static FALLBACK_POOL: OnceCell<PgPool> = OnceCell::const_new();
if let Some(pool) = DB_POOL.with_borrow(Clone::clone) {
return Ok(pool);
let url = DB_URL.as_deref().ok_or(DBError::MissingUrl)?;
FALLBACK_POOL
.get_or_try_init(|| async {
log::debug!("Fallback pool initialization");
let options = PgPoolOptions::new()
.max_connections(ADMIN_MAX_CONNECTIONS)
.acquire_timeout(Duration::from_secs(10));
Ok(options.connect(url).await?)
})
.await
.cloned()
// Production: lazy-init from DATABASE_URL.
static DB_POOL: OnceCell<PgPool> = OnceCell::const_new();
log::debug!("Pool initialization");
// Provisioning helpers. The per-user DSN is derived from the admin
// `DATABASE_URL`. Fully-qualified paths keep these independent of the per-arm
// imports in the `cfg_if!` above so they compile under every cfg (server's own
// `cfg(test)` never provisions, but the pure helpers stay unit-testable).
/// The admin `DATABASE_URL` the server was configured with.
/// # Errors
/// [`DBError::MissingUrl`] if the environment variable is unset.
pub fn admin_database_url() -> Result<String, DBError> {
std::env::var("DATABASE_URL").map_err(|_| DBError::MissingUrl)
/// Runs the migration set on an existing connection.
/// The fleet migrator already holds one per tenant for the claim lock, so
/// opening a second just to migrate would double its connection cost — four
/// workers would spend eight tenant connections against a budget of five
/// (ADR 0032).
/// [`DBError::Migration`] if a migration fails or the applied set has drifted.
pub async fn migrate_on(conn: &mut sqlx::PgConnection) -> Result<(), DBError> {
sqlx::migrate!("../migrations")
.run(conn)
.map_err(DBError::from)
/// Runs the full migration set against the database at `url`. Used by
/// provisioning to bring a freshly-created per-user database up to the
/// (DDL-only) schema.
/// [`DBError::Sqlx`] on connect failure, [`DBError::Migration`] if a migration
/// fails.
pub async fn migrate_url(url: &str) -> Result<(), DBError> {
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(1)
.acquire_timeout(std::time::Duration::from_secs(10))
.connect(url)
let result = sqlx::migrate!("../migrations").run(&pool).await;
// Close the pool on BOTH paths before returning. A still-open connection to
// the per-user DB would otherwise block a compensating `DROP DATABASE` on
// the migration-failure path.
pool.close().await;
result.map_err(DBError::from)
mod db_tests;