use std::collections::BTreeSet;
use sqlx::AnyPool;
use crate::error::{CoreError, CoreResult};
use crate::migration::DbBackend;
use crate::module::{Capability, ModuleRegistry};
#[derive(Debug, Default, Clone)]
pub struct CapabilitySet {
available: BTreeSet<String>,
}
impl CapabilitySet {
pub fn has(&self, name: &str) -> bool {
self.available.contains(name)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.available.iter().map(String::as_str)
}
}
pub async fn check(
pool: &AnyPool,
backend: DbBackend,
registry: &ModuleRegistry,
) -> CoreResult<CapabilitySet> {
let mut wanted: BTreeSet<&str> = BTreeSet::new();
for module in registry.iter() {
wanted.extend(
module
.requires_db_capabilities()
.iter()
.map(Capability::as_str),
);
wanted.extend(
module
.optional_db_capabilities()
.iter()
.map(Capability::as_str),
);
}
let available = detect(pool, backend, &wanted).await?;
for module in registry.iter() {
for cap in module.requires_db_capabilities() {
if !available.contains(cap.as_str()) {
return Err(CoreError::MissingDbCapability {
module: module.id().to_string(),
capability: cap.as_str().to_string(),
backend: backend.name().to_string(),
});
}
}
}
Ok(CapabilitySet {
available: available.into_iter().map(str::to_string).collect(),
})
}
async fn detect<'a>(
pool: &AnyPool,
backend: DbBackend,
wanted: &BTreeSet<&'a str>,
) -> CoreResult<BTreeSet<&'a str>> {
if wanted.is_empty() || backend != DbBackend::Postgres {
return Ok(BTreeSet::new());
}
let rows: Vec<String> = sqlx::query_scalar("SELECT name::text FROM pg_available_extensions")
.fetch_all(pool)
.await?;
let installable: BTreeSet<String> = rows.into_iter().collect();
Ok(wanted
.iter()
.copied()
.filter(|name| installable.contains(*name))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::module::{Module, ModuleId};
async fn sqlite_pool() -> AnyPool {
sqlx::any::install_default_drivers();
sqlx::any::AnyPoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap()
}
struct Needs(ModuleId, &'static [Capability], &'static [Capability]);
impl Module for Needs {
fn id(&self) -> ModuleId {
self.0
}
fn requires_db_capabilities(&self) -> &'static [Capability] {
self.1
}
fn optional_db_capabilities(&self) -> &'static [Capability] {
self.2
}
}
const POSTGIS: &[Capability] = &[Capability::new("postgis")];
const PG_TRGM: &[Capability] = &[Capability::new("pg_trgm")];
const NO_CAPS: &[Capability] = &[];
#[tokio::test]
async fn required_capability_unavailable_errors() {
let pool = sqlite_pool().await;
let mut reg = ModuleRegistry::new();
reg.register(Needs(ModuleId::new("acme.blog"), POSTGIS, NO_CAPS));
let err = check(&pool, DbBackend::Sqlite, ®).await.unwrap_err();
assert!(matches!(err, CoreError::MissingDbCapability { .. }));
}
#[tokio::test]
async fn no_requirements_ok_and_absent_optional_not_in_set() {
let pool = sqlite_pool().await;
let mut reg = ModuleRegistry::new();
reg.register(Needs(ModuleId::new("acme.shop"), NO_CAPS, PG_TRGM));
let caps = check(&pool, DbBackend::Sqlite, ®).await.unwrap();
assert!(!caps.has("pg_trgm"));
}
}