use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::SystemTime;
use tokio_postgres::NoTls;
use crate::backend::{Backend, ReadCursor, StagingCursor};
use crate::gc::{EngineWindowState, WindowCandidate};
use crate::pg_common::test_support::DatabaseJanitor;
use crate::pg_common::{build_pool, qualified_table, quote_identifier, PgOpts};
use crate::Error;
use super::PgLoBackend;
pub(crate) const CONTENT: &[u8] = b"the quick brown fox jumps over the lazy dog";
pub(crate) struct Harness {
pub(crate) dsn: String,
backend: PgLoBackend,
_janitor: DatabaseJanitor,
}
impl Deref for Harness {
type Target = PgLoBackend;
fn deref(&self) -> &PgLoBackend {
&self.backend
}
}
impl DerefMut for Harness {
fn deref_mut(&mut self) -> &mut PgLoBackend {
&mut self.backend
}
}
pub(crate) static DB_SEQ: AtomicU64 = AtomicU64::new(0);
pub(crate) fn fresh_db(tag: &str) -> String {
let nanos = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!(
"abtlo_{}_{}_{}_{nanos:x}",
tag,
std::process::id(),
DB_SEQ.fetch_add(1, Ordering::Relaxed)
)
}
pub(crate) fn env_nonempty(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
pub(crate) fn harness_dsn() -> Option<String> {
let host = env_nonempty("ALKBLOBS_PG_HOST")?;
Some(format!(
"host={host} port={} user={} password={} dbname={}",
env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()),
env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()),
env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()),
env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()),
))
}
pub(crate) fn harness_admin_dsn() -> Option<String> {
let dsn = harness_dsn()?;
Some(format!("{dsn} dbname=postgres"))
}
pub(crate) fn dsn_for_db(db: &str) -> String {
let dsn = harness_dsn().expect("harness dsn");
format!(
"{} dbname={db}",
dsn.rsplit_once("dbname=").expect("dbname").0
)
}
pub(crate) fn skip_note() {
eprintln!("skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB");
}
pub(crate) async fn create_db(admin_dsn: &str, db: &str) {
let (client, conn) = tokio_postgres::connect(admin_dsn, NoTls)
.await
.expect("admin connect");
tokio::spawn(async move {
let _ = conn.await;
});
client
.execute(&format!("CREATE DATABASE {}", quote_identifier(db)), &[])
.await
.expect("create test database");
}
pub(crate) async fn harness(tag: &str) -> Option<Harness> {
harness_with(tag, PgOpts::default()).await
}
pub(crate) async fn harness_with(tag: &str, opts: PgOpts) -> Option<Harness> {
let admin = harness_admin_dsn()?;
let db = fresh_db(tag);
create_db(&admin, &db).await;
let dsn = dsn_for_db(&db);
let backend = PgLoBackend::new(&dsn, opts).await.expect("open backend");
Some(Harness {
dsn,
backend,
_janitor: DatabaseJanitor::new(&admin, &db),
})
}
pub(crate) async fn harness_schemaless(tag: &str) -> Option<Harness> {
let admin = harness_admin_dsn()?;
let db = fresh_db(tag);
create_db(&admin, &db).await;
let dsn = dsn_for_db(&db);
let backend = PgLoBackend::new_schemaless(&dsn, "alkblobs")
.await
.expect("open schemaless backend");
Some(Harness {
dsn,
backend,
_janitor: DatabaseJanitor::new(&admin, &db),
})
}
pub(crate) async fn stored_value(backend: &PgLoBackend, key: &[u8]) -> Option<Vec<u8>> {
let cursor = backend.get(key).await.expect("get succeeds")?;
let (bytes, _) = cursor.read_range(0, cursor.len()).await.expect("read");
Some(bytes)
}
pub(crate) async fn staged(backend: &PgLoBackend, key: &[u8], content: &[u8]) -> Result<(), Error> {
let mut staging = backend.put_staged().await?;
staging.append(content).await?;
staging.commit(key).await
}
pub(crate) async fn staged_in_64k_windows(
backend: &PgLoBackend,
key: &[u8],
content: &[u8],
) -> Result<(), Error> {
let mut staging = backend.put_staged().await?;
for chunk in content.chunks(64 * 1024) {
staging.append(chunk).await?;
}
staging.commit(key).await
}
pub(crate) async fn catalog_loids(backend: &PgLoBackend) -> Vec<u32> {
let client = backend.pool.get().await.expect("checkout");
let rows = client
.query("SELECT DISTINCT loid FROM pg_largeobject", &[])
.await
.expect("catalog loids");
let mut loids: Vec<u32> = rows.iter().map(|row| row.get::<_, u32>(0)).collect();
loids.sort();
loids
}
pub(crate) async fn catalog_rows(backend: &PgLoBackend) -> i64 {
let client = backend.pool.get().await.expect("checkout");
client
.query_one("SELECT count(*) FROM pg_largeobject", &[])
.await
.expect("catalog count")
.get::<_, i64>(0)
}
pub(crate) async fn companion_loids_sorted(backend: &PgLoBackend) -> Vec<u32> {
let client = backend.pool.get().await.expect("checkout");
let sql = format!("SELECT loid FROM {} ORDER BY loid", backend.table);
let rows = client
.query(sql.as_str(), &[])
.await
.expect("companion loids");
rows.iter().map(|row| row.get::<_, u32>(0)).collect()
}
pub(crate) async fn shown_synchronous_commit(backend: &PgLoBackend) -> String {
let client = backend.pool.get().await.expect("checkout");
let messages = client
.simple_query("SHOW synchronous_commit")
.await
.expect("SHOW");
for message in &messages {
if let tokio_postgres::SimpleQueryMessage::Row(row) = message {
return row.get(0).expect("SHOW row").to_owned();
}
}
panic!("SHOW returned no row: {messages:?}")
}
pub(crate) fn pattern(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
pub(crate) fn pages_for(len: usize) -> i64 {
(len as i64 + 2047) / 2048
}
pub(crate) fn nanos_to_time(n: i64) -> SystemTime {
if n >= 0 {
std::time::UNIX_EPOCH + std::time::Duration::from_nanos(n as u64)
} else {
std::time::UNIX_EPOCH - std::time::Duration::from_nanos((-n) as u64)
}
}
pub(crate) fn at(nanos: i64) -> SystemTime {
nanos_to_time(nanos)
}
pub(crate) async fn harness_gc(tag: &str) -> Option<Harness> {
let backend = harness(tag).await?;
backend.ensure_gc_schema().await.unwrap();
Some(backend)
}
pub(crate) fn stages(key: &[u8], visible_after: SystemTime) -> WindowCandidate {
WindowCandidate {
key: key.to_vec(),
visible_after,
}
}
pub(crate) async fn window_rows(backend: &PgLoBackend) -> Vec<(Vec<u8>, i64)> {
let client = backend.pool.get().await.expect("checkout");
let sql = format!(
"SELECT key, visible_after FROM {} ORDER BY key",
backend.window_table
);
let rows = client.query(sql.as_str(), &[]).await.expect("window rows");
let mut out: Vec<(Vec<u8>, i64)> = rows
.iter()
.map(|row| (row.get::<_, Vec<u8>>(0), row.get::<_, i64>(1)))
.collect();
out.sort();
out
}
pub(crate) async fn schema_tables(backend: &PgLoBackend) -> Vec<String> {
let client = backend.pool.get().await.expect("checkout");
let rows = client
.query(
"SELECT table_name::text FROM information_schema.tables \
WHERE table_schema = $1 ORDER BY table_name",
&[&backend.schema],
)
.await
.expect("schema tables");
rows.iter().map(|row| row.get::<_, String>(0)).collect()
}
#[cfg(test)]
impl PgLoBackend {
#[cfg(test)]
pub(crate) async fn new_schemaless(config: &str, schema: &str) -> Result<Self, Error> {
let pool = build_pool(config, &PgOpts::default())?;
Ok(Self {
pool,
table: qualified_table(schema, "lo_entries"),
window_table: qualified_table(schema, "gc_window"),
schema: schema.to_owned(),
orphan_reap_permitted: false,
#[cfg(test)]
fail_commit: false,
#[cfg(test)]
fail_window_stage: false,
})
}
}