mod database_per_tenant;
mod schema_per_tenant;
mod shared_schema;
pub use database_per_tenant::{DatabasePerTenantConfig, DatabasePerTenantStrategy};
pub use schema_per_tenant::{SchemaPerTenantConfig, SchemaPerTenantStrategy};
pub use shared_schema::{SharedSchemaConfig, SharedSchemaStrategy};
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::tenant::TenantId;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TenancyStrategy {
SharedSchema(SharedSchemaConfig),
SchemaPerTenant(SchemaPerTenantConfig),
DatabasePerTenant(DatabasePerTenantConfig),
}
impl Default for TenancyStrategy {
fn default() -> Self {
TenancyStrategy::SharedSchema(SharedSchemaConfig::default())
}
}
impl fmt::Display for TenancyStrategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TenancyStrategy::SharedSchema(_) => write!(f, "shared-schema"),
TenancyStrategy::SchemaPerTenant(_) => write!(f, "schema-per-tenant"),
TenancyStrategy::DatabasePerTenant(_) => write!(f, "database-per-tenant"),
}
}
}
impl TenancyStrategy {
pub fn isolation_level(&self) -> IsolationLevel {
match self {
TenancyStrategy::SharedSchema(_) => IsolationLevel::Logical,
TenancyStrategy::SchemaPerTenant(_) => IsolationLevel::Schema,
TenancyStrategy::DatabasePerTenant(_) => IsolationLevel::Physical,
}
}
pub fn uses_shared_pool(&self) -> bool {
match self {
TenancyStrategy::SharedSchema(_) => true,
TenancyStrategy::SchemaPerTenant(_) => true,
TenancyStrategy::DatabasePerTenant(config) => !config.pool_per_tenant,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IsolationLevel {
Logical,
Schema,
Physical,
}
impl fmt::Display for IsolationLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IsolationLevel::Logical => write!(f, "logical"),
IsolationLevel::Schema => write!(f, "schema"),
IsolationLevel::Physical => write!(f, "physical"),
}
}
}
pub trait TenantResolver: Send + Sync {
fn resolve(&self, tenant_id: &TenantId) -> TenantResolution;
fn validate(&self, tenant_id: &TenantId) -> Result<(), TenantValidationError>;
fn system_tenant(&self) -> TenantResolution;
}
#[derive(Debug, Clone)]
pub enum TenantResolution {
SharedSchema {
tenant_id: String,
},
Schema {
schema_name: String,
},
Database {
connection: String,
},
}
#[derive(Debug, Clone)]
pub struct TenantValidationError {
pub tenant_id: String,
pub reason: String,
}
impl fmt::Display for TenantValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid tenant '{}': {}", self.tenant_id, self.reason)
}
}
impl std::error::Error for TenantValidationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tenancy_strategy_display() {
let shared = TenancyStrategy::SharedSchema(SharedSchemaConfig::default());
assert_eq!(shared.to_string(), "shared-schema");
let schema = TenancyStrategy::SchemaPerTenant(SchemaPerTenantConfig::default());
assert_eq!(schema.to_string(), "schema-per-tenant");
let db = TenancyStrategy::DatabasePerTenant(DatabasePerTenantConfig::default());
assert_eq!(db.to_string(), "database-per-tenant");
}
#[test]
fn test_isolation_level() {
let shared = TenancyStrategy::SharedSchema(SharedSchemaConfig::default());
assert_eq!(shared.isolation_level(), IsolationLevel::Logical);
let schema = TenancyStrategy::SchemaPerTenant(SchemaPerTenantConfig::default());
assert_eq!(schema.isolation_level(), IsolationLevel::Schema);
let db = TenancyStrategy::DatabasePerTenant(DatabasePerTenantConfig::default());
assert_eq!(db.isolation_level(), IsolationLevel::Physical);
}
#[test]
fn test_uses_shared_pool() {
let shared = TenancyStrategy::SharedSchema(SharedSchemaConfig::default());
assert!(shared.uses_shared_pool());
let db_shared_pool = TenancyStrategy::DatabasePerTenant(DatabasePerTenantConfig {
pool_per_tenant: false,
..Default::default()
});
assert!(db_shared_pool.uses_shared_pool());
let db_per_pool = TenancyStrategy::DatabasePerTenant(DatabasePerTenantConfig {
pool_per_tenant: true,
..Default::default()
});
assert!(!db_per_pool.uses_shared_pool());
}
#[test]
fn test_tenant_validation_error_display() {
let err = TenantValidationError {
tenant_id: "bad-tenant".to_string(),
reason: "contains invalid characters".to_string(),
};
assert!(err.to_string().contains("bad-tenant"));
assert!(err.to_string().contains("invalid characters"));
}
}