use color_eyre::eyre::WrapErr;
use tracing::info_span;
use super::connection::{connect_admin, escape_identifier};
use crate::{error::BootstrapResult, observability::LOG_TARGET};
#[derive(Debug)]
pub struct TemporaryDatabase {
name: String,
admin_url: String,
database_url: String,
}
impl TemporaryDatabase {
pub(crate) const fn new(name: String, admin_url: String, database_url: String) -> Self {
Self {
name,
admin_url,
database_url,
}
}
#[must_use]
pub fn name(&self) -> &str { &self.name }
#[must_use]
pub fn url(&self) -> &str { &self.database_url }
pub fn drop_database(self) -> BootstrapResult<()> { self.try_drop() }
pub fn force_drop(self) -> BootstrapResult<()> {
let _span = info_span!("force_drop_database", db = %self.name).entered();
let mut client = connect_admin(&self.admin_url)?;
client
.execute(
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND \
pid <> pg_backend_pid()",
&[&self.name],
)
.wrap_err(format!(
"failed to terminate connections to database '{}'",
self.name
))
.map_err(crate::error::BootstrapError::from)?;
let escaped = escape_identifier(&self.name);
let drop_sql = format!("DROP DATABASE \"{escaped}\"");
client
.batch_execute(&drop_sql)
.wrap_err(format!("failed to drop database '{}'", self.name))
.map_err(crate::error::BootstrapError::from)?;
Ok(())
}
fn try_drop(&self) -> BootstrapResult<()> {
let _span = info_span!("drop_database", db = %self.name).entered();
let mut client = connect_admin(&self.admin_url)?;
let escaped = escape_identifier(&self.name);
let sql = format!("DROP DATABASE \"{escaped}\"");
client
.batch_execute(&sql)
.wrap_err(format!("failed to drop database '{}'", self.name))
.map_err(crate::error::BootstrapError::from)?;
Ok(())
}
}
impl Drop for TemporaryDatabase {
fn drop(&mut self) {
if let Err(e) = self.try_drop() {
tracing::warn!(
target: LOG_TARGET,
db = %self.name,
error = ?e,
"failed to drop temporary database"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn temporary_database_accessors() {
let temp = TemporaryDatabase::new(
"test_db".to_owned(),
"postgresql://user:pass@localhost:5432/postgres".to_owned(),
"postgresql://user:pass@localhost:5432/test_db".to_owned(),
);
assert_eq!(temp.name(), "test_db");
assert!(temp.url().contains("test_db"));
}
#[test]
fn drop_database_returns_error_on_connection_failure() {
let temp = TemporaryDatabase::new(
"test_db".to_owned(),
"postgresql://user:pass@localhost:59999/postgres".to_owned(),
"postgresql://user:pass@localhost:59999/test_db".to_owned(),
);
let result = temp.drop_database();
let Err(err) = result else {
panic!("expected error when database unreachable");
};
let err_str = err.to_string();
assert!(
err_str.contains("failed to connect"),
"expected connection failure, got: {err_str}"
);
}
#[test]
fn force_drop_returns_error_on_connection_failure() {
let temp = TemporaryDatabase::new(
"test_db".to_owned(),
"postgresql://user:pass@localhost:59999/postgres".to_owned(),
"postgresql://user:pass@localhost:59999/test_db".to_owned(),
);
let result = temp.force_drop();
let Err(err) = result else {
panic!("expected error when database unreachable");
};
let err_str = err.to_string();
assert!(
err_str.contains("failed to connect"),
"expected connection failure, got: {err_str}"
);
}
#[test]
fn drop_trait_does_not_panic_on_connection_failure() {
let temp = TemporaryDatabase::new(
"test_db".to_owned(),
"postgresql://user:pass@localhost:59999/postgres".to_owned(),
"postgresql://user:pass@localhost:59999/test_db".to_owned(),
);
drop(temp);
}
}