helios_persistence/strategy/
mod.rs1mod database_per_tenant;
85mod schema_per_tenant;
86mod shared_schema;
87
88pub use database_per_tenant::{DatabasePerTenantConfig, DatabasePerTenantStrategy};
89pub use schema_per_tenant::{SchemaPerTenantConfig, SchemaPerTenantStrategy};
90pub use shared_schema::{SharedSchemaConfig, SharedSchemaStrategy};
91
92use std::fmt;
93
94use serde::{Deserialize, Serialize};
95
96use crate::tenant::TenantId;
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(tag = "type", rename_all = "snake_case")]
103pub enum TenancyStrategy {
104 SharedSchema(SharedSchemaConfig),
106
107 SchemaPerTenant(SchemaPerTenantConfig),
109
110 DatabasePerTenant(DatabasePerTenantConfig),
112}
113
114impl Default for TenancyStrategy {
115 fn default() -> Self {
116 TenancyStrategy::SharedSchema(SharedSchemaConfig::default())
117 }
118}
119
120impl fmt::Display for TenancyStrategy {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 match self {
123 TenancyStrategy::SharedSchema(_) => write!(f, "shared-schema"),
124 TenancyStrategy::SchemaPerTenant(_) => write!(f, "schema-per-tenant"),
125 TenancyStrategy::DatabasePerTenant(_) => write!(f, "database-per-tenant"),
126 }
127 }
128}
129
130impl TenancyStrategy {
131 pub fn isolation_level(&self) -> IsolationLevel {
133 match self {
134 TenancyStrategy::SharedSchema(_) => IsolationLevel::Logical,
135 TenancyStrategy::SchemaPerTenant(_) => IsolationLevel::Schema,
136 TenancyStrategy::DatabasePerTenant(_) => IsolationLevel::Physical,
137 }
138 }
139
140 pub fn uses_shared_pool(&self) -> bool {
142 match self {
143 TenancyStrategy::SharedSchema(_) => true,
144 TenancyStrategy::SchemaPerTenant(_) => true,
145 TenancyStrategy::DatabasePerTenant(config) => !config.pool_per_tenant,
146 }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum IsolationLevel {
153 Logical,
155 Schema,
157 Physical,
159}
160
161impl fmt::Display for IsolationLevel {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 IsolationLevel::Logical => write!(f, "logical"),
165 IsolationLevel::Schema => write!(f, "schema"),
166 IsolationLevel::Physical => write!(f, "physical"),
167 }
168 }
169}
170
171pub trait TenantResolver: Send + Sync {
176 fn resolve(&self, tenant_id: &TenantId) -> TenantResolution;
178
179 fn validate(&self, tenant_id: &TenantId) -> Result<(), TenantValidationError>;
181
182 fn system_tenant(&self) -> TenantResolution;
184}
185
186#[derive(Debug, Clone)]
188pub enum TenantResolution {
189 SharedSchema {
191 tenant_id: String,
193 },
194
195 Schema {
197 schema_name: String,
199 },
200
201 Database {
203 connection: String,
205 },
206}
207
208#[derive(Debug, Clone)]
210pub struct TenantValidationError {
211 pub tenant_id: String,
213 pub reason: String,
215}
216
217impl fmt::Display for TenantValidationError {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 write!(f, "invalid tenant '{}': {}", self.tenant_id, self.reason)
220 }
221}
222
223impl std::error::Error for TenantValidationError {}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_tenancy_strategy_display() {
231 let shared = TenancyStrategy::SharedSchema(SharedSchemaConfig::default());
232 assert_eq!(shared.to_string(), "shared-schema");
233
234 let schema = TenancyStrategy::SchemaPerTenant(SchemaPerTenantConfig::default());
235 assert_eq!(schema.to_string(), "schema-per-tenant");
236
237 let db = TenancyStrategy::DatabasePerTenant(DatabasePerTenantConfig::default());
238 assert_eq!(db.to_string(), "database-per-tenant");
239 }
240
241 #[test]
242 fn test_isolation_level() {
243 let shared = TenancyStrategy::SharedSchema(SharedSchemaConfig::default());
244 assert_eq!(shared.isolation_level(), IsolationLevel::Logical);
245
246 let schema = TenancyStrategy::SchemaPerTenant(SchemaPerTenantConfig::default());
247 assert_eq!(schema.isolation_level(), IsolationLevel::Schema);
248
249 let db = TenancyStrategy::DatabasePerTenant(DatabasePerTenantConfig::default());
250 assert_eq!(db.isolation_level(), IsolationLevel::Physical);
251 }
252
253 #[test]
254 fn test_uses_shared_pool() {
255 let shared = TenancyStrategy::SharedSchema(SharedSchemaConfig::default());
256 assert!(shared.uses_shared_pool());
257
258 let db_shared_pool = TenancyStrategy::DatabasePerTenant(DatabasePerTenantConfig {
259 pool_per_tenant: false,
260 ..Default::default()
261 });
262 assert!(db_shared_pool.uses_shared_pool());
263
264 let db_per_pool = TenancyStrategy::DatabasePerTenant(DatabasePerTenantConfig {
265 pool_per_tenant: true,
266 ..Default::default()
267 });
268 assert!(!db_per_pool.uses_shared_pool());
269 }
270
271 #[test]
272 fn test_tenant_validation_error_display() {
273 let err = TenantValidationError {
274 tenant_id: "bad-tenant".to_string(),
275 reason: "contains invalid characters".to_string(),
276 };
277 assert!(err.to_string().contains("bad-tenant"));
278 assert!(err.to_string().contains("invalid characters"));
279 }
280}