use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use super::error::{StorageError, StorageResult};
pub async fn has_timescaledb_extension(pool: &PgPool) -> StorageResult<bool> {
sqlx::query_scalar::<_, bool>("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb')")
.fetch_one(pool)
.await
.map_err(|e| StorageError::QueryFailed(format!("pg_extension probe: {e}")))
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TimescaleStats {
pub total_chunks: u32,
pub compressed_chunks: u32,
pub compression_ratio_tenths: u32,
pub oldest_chunk_age_days: u32,
}
pub(crate) async fn query_timescale_stats(pool: &PgPool) -> StorageResult<TimescaleStats> {
let (total, compressed, oldest_age_days): (i64, i64, i32) = sqlx::query_as(
"SELECT \
COUNT(*)::bigint, \
COUNT(*) FILTER (WHERE is_compressed)::bigint, \
COALESCE(EXTRACT(DAY FROM NOW() - MIN(range_start))::int, 0) \
FROM timescaledb_information.chunks \
WHERE hypertable_name IN ('audit_events', 'metrics')",
)
.fetch_one(pool)
.await
.map_err(|e| StorageError::QueryFailed(format!("timescale chunks rollup: {e}")))?;
Ok(TimescaleStats {
total_chunks: u32::try_from(total).unwrap_or(u32::MAX),
compressed_chunks: u32::try_from(compressed).unwrap_or(u32::MAX),
compression_ratio_tenths: 0,
oldest_chunk_age_days: u32::try_from(oldest_age_days).unwrap_or(0),
})
}
#[cfg(test)]
mod tests {
use super::*;
use sqlx::postgres::PgPoolOptions;
async fn pool_or_skip() -> Option<PgPool> {
let url = match std::env::var("AAASM_DATABASE_URL") {
Ok(v) => v,
Err(_) => {
eprintln!(
"skipping postgres test: AAASM_DATABASE_URL not set (CI provides this via services: postgres)"
);
return None;
}
};
Some(
PgPoolOptions::new()
.max_connections(2)
.connect(&url)
.await
.expect("connect to AAASM_DATABASE_URL"),
)
}
#[tokio::test]
async fn probe_returns_false_on_plain_postgres() {
if std::env::var("TIMESCALEDB_AVAILABLE").as_deref() == Ok("1") {
eprintln!(
"skipping plain-postgres probe test: TIMESCALEDB_AVAILABLE=1 (see probe_returns_true_on_timescaledb)"
);
return;
}
let Some(pool) = pool_or_skip().await else {
return;
};
let present = has_timescaledb_extension(&pool)
.await
.expect("probe must succeed on plain PostgreSQL");
assert!(
!present,
"expected probe to report false on plain PostgreSQL; if your CI installed TimescaleDB, set TIMESCALEDB_AVAILABLE=1"
);
}
#[tokio::test]
async fn probe_returns_true_on_timescaledb() {
if std::env::var("TIMESCALEDB_AVAILABLE").as_deref() != Ok("1") {
eprintln!(
"skipping timescaledb probe test: TIMESCALEDB_AVAILABLE != 1 (set it to 1 when AAASM_DATABASE_URL points at a TimescaleDB-enabled instance)"
);
return;
}
let Some(pool) = pool_or_skip().await else {
return;
};
let present = has_timescaledb_extension(&pool)
.await
.expect("probe must succeed on a TimescaleDB-enabled PostgreSQL");
assert!(
present,
"TIMESCALEDB_AVAILABLE=1 was set but the extension is not installed; \
check the docker image is timescale/timescaledb:latest-pg17"
);
}
}