use std::time::{SystemTime, UNIX_EPOCH};
use deadpool_postgres::{Manager, ManagerConfig, Object, Pool, RecyclingMethod};
use tokio_postgres::NoTls;
use crate::Error;
pub(crate) fn err(e: impl std::fmt::Display) -> Error {
Error::Io(format!("postgres: {e}"))
}
pub(crate) fn pg_err(e: tokio_postgres::Error) -> Error {
match e.as_db_error() {
Some(db) => Error::Io(format!("postgres: {db}")),
None => err(e),
}
}
pub(crate) fn is_undefined_table(e: &tokio_postgres::Error) -> bool {
e.code() == Some(&tokio_postgres::error::SqlState::UNDEFINED_TABLE)
}
pub(crate) fn quote_identifier(name: &str) -> String {
let mut quoted = String::with_capacity(name.len() + 2);
quoted.push('"');
for c in name.chars() {
if c == '"' {
quoted.push('"');
}
quoted.push(c);
}
quoted.push('"');
quoted
}
pub(crate) fn qualified_table(schema: &str, table: &str) -> String {
format!("{}.{}", quote_identifier(schema), quote_identifier(table))
}
pub(crate) const DEFAULT_SCHEMA: &str = "alkblobs";
pub(crate) const DEFAULT_MAX_SIZE: usize = 8;
pub(crate) fn size_u64(raw: i64) -> u64 {
raw.max(0) as u64
}
pub(crate) fn time_to_nanos(t: SystemTime) -> i64 {
match t.duration_since(UNIX_EPOCH) {
Ok(d) => d.as_nanos() as i64,
Err(e) => -(e.duration().as_nanos() as i64),
}
}
#[cfg_attr(
not(any(feature = "postgres", all(feature = "pg-lo", feature = "mem"))),
allow(dead_code)
)]
pub(crate) fn build_pool(config: &str, opts: &PgOpts) -> Result<Pool, Error> {
if opts.max_size == 0 {
return Err(Error::Io(format!(
"postgres: max_size must be positive, got {}",
opts.max_size
)));
}
let mut cfg: tokio_postgres::Config = config.parse().map_err(err)?;
let engine_opt = format!(
"-c synchronous_commit={}",
if opts.synchronous_commit { "on" } else { "off" }
);
let merged = match cfg.get_options() {
Some(existing) => format!("{existing} {engine_opt}"),
None => engine_opt,
};
cfg.options(merged);
cfg.application_name(&opts.application_name);
let manager = Manager::from_config(
cfg,
NoTls,
ManagerConfig {
recycling_method: RecyclingMethod::Fast,
},
);
Pool::builder(manager)
.max_size(opts.max_size)
.build()
.map_err(err)
}
pub(crate) fn detached_batch_execute(client: Object, sql: String) {
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
let _ = client.batch_execute(&sql).await;
});
}
Err(_) => {
let _ = std::thread::spawn(move || {
let Ok(rt) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
return;
};
rt.block_on(async move {
let _ = client.batch_execute(&sql).await;
});
})
.join();
}
}
}
#[derive(Debug, Clone)]
pub struct PgOpts {
pub schema: String,
pub max_size: usize,
pub synchronous_commit: bool,
pub orphan_reap_permitted: bool,
pub application_name: String,
}
impl Default for PgOpts {
fn default() -> Self {
Self {
schema: DEFAULT_SCHEMA.to_owned(),
max_size: DEFAULT_MAX_SIZE,
synchronous_commit: true,
orphan_reap_permitted: false,
application_name: "alkblobs".to_owned(),
}
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::quote_identifier;
use tokio_postgres::NoTls;
#[cfg(feature = "postgres")]
pub(crate) struct SchemaJanitor {
dsn: String,
schema: String,
}
#[cfg(feature = "postgres")]
impl SchemaJanitor {
pub(crate) fn new(dsn: &str, schema: &str) -> SchemaJanitor {
SchemaJanitor {
dsn: dsn.to_owned(),
schema: schema.to_owned(),
}
}
}
#[cfg(feature = "postgres")]
impl Drop for SchemaJanitor {
fn drop(&mut self) {
let dsn = self.dsn.clone();
let schema = self.schema.clone();
let _ = std::thread::spawn(move || {
let Ok(rt) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
return;
};
rt.block_on(async move {
let Ok((client, conn)) = tokio_postgres::connect(&dsn, NoTls).await else {
return;
};
tokio::spawn(conn);
let sql = format!(
"DROP SCHEMA IF EXISTS {} CASCADE",
quote_identifier(&schema)
);
let _ = client.execute(sql.as_str(), &[]).await;
});
})
.join();
}
}
#[cfg(feature = "pg-lo")]
pub(crate) struct DatabaseJanitor {
admin_dsn: String,
db: String,
}
#[cfg(feature = "pg-lo")]
impl DatabaseJanitor {
pub(crate) fn new(admin_dsn: &str, db: &str) -> DatabaseJanitor {
DatabaseJanitor {
admin_dsn: admin_dsn.to_owned(),
db: db.to_owned(),
}
}
}
#[cfg(feature = "pg-lo")]
impl Drop for DatabaseJanitor {
fn drop(&mut self) {
let admin_dsn = self.admin_dsn.clone();
let db = self.db.clone();
let _ = std::thread::spawn(move || {
let Ok(rt) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
return;
};
rt.block_on(async move {
let Ok((client, conn)) = tokio_postgres::connect(&admin_dsn, NoTls).await
else {
return;
};
tokio::spawn(conn);
let sql = format!(
"DROP DATABASE IF EXISTS {} WITH (FORCE)",
quote_identifier(&db)
);
let _ = client.execute(sql.as_str(), &[]).await;
});
})
.join();
}
}
}