use fraiseql_core::db::traits::DatabaseAdapter;
use fraiseql_error::{FraiseQLError, Result};
pub(crate) const MAX_PG_IDENTIFIER_LEN: usize = 63;
pub(crate) const TENANT_SCHEMA_PREFIX: &str = "tenant_";
pub fn tenant_schema_name(key: &str) -> Result<String> {
if key.is_empty() {
return Err(FraiseQLError::validation("Tenant key must not be empty for schema isolation"));
}
if !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(FraiseQLError::validation(format!(
"Tenant key '{key}' contains invalid characters. \
Only ASCII alphanumeric and underscore are allowed for schema isolation."
)));
}
let schema_name = format!("{TENANT_SCHEMA_PREFIX}{key}");
if schema_name.len() > MAX_PG_IDENTIFIER_LEN {
return Err(FraiseQLError::validation(format!(
"Tenant schema name '{schema_name}' exceeds PostgreSQL's \
{MAX_PG_IDENTIFIER_LEN}-character identifier limit. \
Use a shorter tenant key."
)));
}
Ok(schema_name)
}
pub fn search_path_sql(key: &str) -> Result<String> {
let schema_name = tenant_schema_name(key)?;
Ok(format!("SET search_path TO {schema_name}, public"))
}
pub fn create_schema_ddl(key: &str) -> Result<String> {
let schema_name = tenant_schema_name(key)?;
Ok(format!("CREATE SCHEMA IF NOT EXISTS {schema_name}"))
}
pub fn drop_schema_ddl(key: &str) -> Result<String> {
let schema_name = tenant_schema_name(key)?;
Ok(format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"))
}
pub async fn provision_tenant_schema(key: &str, adapter: &dyn DatabaseAdapter) -> Result<()> {
let ddl = create_schema_ddl(key)?;
adapter.execute_raw_query(&ddl).await.map_err(|e| {
FraiseQLError::database(format!("Failed to provision schema for tenant '{key}': {e}"))
})?;
Ok(())
}
pub async fn drop_tenant_schema(key: &str, adapter: &dyn DatabaseAdapter) -> Result<()> {
let ddl = drop_schema_ddl(key)?;
adapter.execute_raw_query(&ddl).await.map_err(|e| {
FraiseQLError::database(format!("Failed to drop schema for tenant '{key}': {e}"))
})?;
Ok(())
}
pub async fn configure_search_path(key: &str, adapter: &dyn DatabaseAdapter) -> Result<()> {
let sql = search_path_sql(key)?;
adapter.execute_raw_query(&sql).await.map_err(|e| {
FraiseQLError::database(format!("Failed to set search_path for tenant '{key}': {e}"))
})?;
Ok(())
}