1use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use serde::{Deserialize, Serialize};
11
12use crate::db_type::DbType;
13use crate::pool::Pool;
14
15fn now_ms() -> u64 {
16 SystemTime::now()
17 .duration_since(UNIX_EPOCH)
18 .map(|d| d.as_millis() as u64)
19 .unwrap_or(0)
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub enum ConnectionLevelIsolation {
25 SetTenantId,
27 SchemaIsolation,
29 ConnectionBinding,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub enum ConnectionAffinityPolicy {
36 Strict,
38 Preferred,
40 None,
42}
43
44#[derive(Debug, Clone)]
46pub struct ConnectionLevelTenantConfig {
47 pub isolation: ConnectionLevelIsolation,
49 pub affinity_policy: ConnectionAffinityPolicy,
51 pub affinity_timeout_ms: u64,
53 pub db_type: DbType,
55}
56
57impl ConnectionLevelTenantConfig {
58 pub fn new(db_type: DbType) -> Self {
60 Self {
61 isolation: ConnectionLevelIsolation::SetTenantId,
62 affinity_policy: ConnectionAffinityPolicy::Preferred,
63 affinity_timeout_ms: 5_000,
64 db_type,
65 }
66 }
67
68 pub fn with_isolation(mut self, isolation: ConnectionLevelIsolation) -> Self {
70 self.isolation = isolation;
71 self
72 }
73
74 pub fn with_affinity_policy(mut self, policy: ConnectionAffinityPolicy) -> Self {
76 self.affinity_policy = policy;
77 self
78 }
79
80 pub fn with_affinity_timeout_ms(mut self, ms: u64) -> Self {
82 self.affinity_timeout_ms = ms;
83 self
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum TenantError {
90 NoBoundConnection,
92 TamperingRejected,
94 CleanupFailed,
96 UnsupportedDialect,
98 EmptyTenantId,
100}
101
102impl std::fmt::Display for TenantError {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 match self {
105 TenantError::NoBoundConnection => write!(f, "no connection bound to tenant"),
106 TenantError::TamperingRejected => write!(f, "tenant context tampering rejected"),
107 TenantError::CleanupFailed => write!(f, "tenant context cleanup failed"),
108 TenantError::UnsupportedDialect => {
109 write!(f, "unsupported dialect for SET app.tenant_id")
110 }
111 TenantError::EmptyTenantId => write!(f, "tenant_id is empty"),
112 }
113 }
114}
115
116impl std::error::Error for TenantError {}
117
118pub type ConnectionId = u64;
120
121#[derive(Debug, Clone)]
123pub struct TenantBinding {
124 pub connection_id: ConnectionId,
126 pub tenant_id: String,
128 pub bound_at: u64,
130}
131
132pub struct ConnectionTenantBinder {
134 pool: Arc<Pool>,
135 config: ConnectionLevelTenantConfig,
136 tenant_bindings: Mutex<HashMap<String, Vec<ConnectionId>>>,
137 next_connection_id: Mutex<u64>,
138}
139
140impl ConnectionTenantBinder {
141 pub fn new(pool: Arc<Pool>, config: ConnectionLevelTenantConfig) -> Self {
143 Self {
144 pool,
145 config,
146 tenant_bindings: Mutex::new(HashMap::new()),
147 next_connection_id: Mutex::new(1),
148 }
149 }
150
151 pub fn config(&self) -> &ConnectionLevelTenantConfig {
153 &self.config
154 }
155
156 pub fn supports_set_tenant_id(&self) -> bool {
158 matches!(self.config.db_type, DbType::PostgreSQL | DbType::MySQL)
159 }
160
161 pub fn build_set_tenant_sql(&self, tenant_id: &str) -> String {
163 format!("SET app.tenant_id = '{}'", tenant_id)
164 }
165
166 pub fn build_clear_tenant_sql(&self) -> String {
168 "SET app.tenant_id = NULL".to_string()
169 }
170
171 pub fn bind_connection(&self, tenant_id: &str) -> ConnectionId {
173 let conn_id = {
174 let mut next = self
175 .next_connection_id
176 .lock()
177 .unwrap_or_else(|e| e.into_inner());
178 let id = *next;
179 *next += 1;
180 id
181 };
182 let mut bindings = self
183 .tenant_bindings
184 .lock()
185 .unwrap_or_else(|e| e.into_inner());
186 bindings
187 .entry(tenant_id.to_string())
188 .or_default()
189 .push(conn_id);
190 conn_id
191 }
192
193 pub fn find_bound_connections(&self, tenant_id: &str) -> Vec<ConnectionId> {
195 let bindings = self
196 .tenant_bindings
197 .lock()
198 .unwrap_or_else(|e| e.into_inner());
199 bindings.get(tenant_id).cloned().unwrap_or_default()
200 }
201
202 pub fn unbind_connection(&self, tenant_id: &str, conn_id: ConnectionId) {
204 let mut bindings = self
205 .tenant_bindings
206 .lock()
207 .unwrap_or_else(|e| e.into_inner());
208 if let Some(conns) = bindings.get_mut(tenant_id) {
209 conns.retain(|&id| id != conn_id);
210 }
211 }
212
213 pub fn binding_count(&self, tenant_id: &str) -> usize {
215 let bindings = self
216 .tenant_bindings
217 .lock()
218 .unwrap_or_else(|e| e.into_inner());
219 bindings.get(tenant_id).map(|v| v.len()).unwrap_or(0)
220 }
221
222 pub fn all_bindings(&self) -> Vec<TenantBinding> {
224 let bindings = self
225 .tenant_bindings
226 .lock()
227 .unwrap_or_else(|e| e.into_inner());
228 let mut result = Vec::new();
229 for (tenant_id, conn_ids) in bindings.iter() {
230 for &conn_id in conn_ids {
231 result.push(TenantBinding {
232 connection_id: conn_id,
233 tenant_id: tenant_id.clone(),
234 bound_at: now_ms(),
235 });
236 }
237 }
238 result
239 }
240
241 pub fn pool(&self) -> &Arc<Pool> {
243 &self.pool
244 }
245
246 pub fn validate_tenant_id(&self, tenant_id: &str) -> Result<(), TenantError> {
248 if tenant_id.is_empty() {
249 return Err(TenantError::EmptyTenantId);
250 }
251 Ok(())
252 }
253
254 pub fn resolve_isolation(&self) -> ConnectionLevelIsolation {
256 if self.config.isolation == ConnectionLevelIsolation::SetTenantId
257 && !self.supports_set_tenant_id()
258 {
259 ConnectionLevelIsolation::SchemaIsolation
260 } else {
261 self.config.isolation.clone()
262 }
263 }
264}
265
266pub struct TenantConnectionGuard {
268 binder: Arc<ConnectionTenantBinder>,
269 tenant_id: String,
270 connection_id: ConnectionId,
271 active: bool,
272}
273
274impl TenantConnectionGuard {
275 pub fn new(
277 binder: Arc<ConnectionTenantBinder>,
278 tenant_id: String,
279 connection_id: ConnectionId,
280 ) -> Self {
281 Self {
282 binder,
283 tenant_id,
284 connection_id,
285 active: true,
286 }
287 }
288
289 pub fn tenant_id(&self) -> &str {
291 &self.tenant_id
292 }
293
294 pub fn connection_id(&self) -> ConnectionId {
296 self.connection_id
297 }
298
299 pub fn is_active(&self) -> bool {
301 self.active
302 }
303
304 pub fn clear_tenant_sql(&self) -> String {
306 self.binder.build_clear_tenant_sql()
307 }
308
309 pub fn release(&mut self) -> Result<(), TenantError> {
311 if !self.active {
312 return Ok(());
313 }
314 self.binder
315 .unbind_connection(&self.tenant_id, self.connection_id);
316 self.active = false;
317 Ok(())
318 }
319}
320
321impl Drop for TenantConnectionGuard {
322 fn drop(&mut self) {
323 if self.active {
324 self.binder
325 .unbind_connection(&self.tenant_id, self.connection_id);
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::pool::{ConnectionFactory, PoolConfigBuilder};
334
335 struct MockFactory;
336
337 #[async_trait::async_trait]
338 impl ConnectionFactory for MockFactory {
339 async fn create(&self) -> Result<Box<dyn crate::pool::Connection>, crate::DbError> {
340 Err(crate::DbError::PoolError(
341 crate::error::PoolError::InvalidConfig("mock".to_string()),
342 ))
343 }
344 }
345
346 fn make_pool() -> Arc<Pool> {
347 let config = PoolConfigBuilder::new().max_size(1).build().unwrap();
348 let factory: Arc<dyn ConnectionFactory> = Arc::new(MockFactory);
349 Arc::new(Pool::new(config, factory).unwrap())
350 }
351
352 #[test]
353 fn test_config_default() {
354 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
355 assert_eq!(config.isolation, ConnectionLevelIsolation::SetTenantId);
356 assert_eq!(config.affinity_policy, ConnectionAffinityPolicy::Preferred);
357 assert_eq!(config.affinity_timeout_ms, 5_000);
358 assert_eq!(config.db_type, DbType::PostgreSQL);
359 }
360
361 #[test]
362 fn test_config_builder() {
363 let config = ConnectionLevelTenantConfig::new(DbType::MySQL)
364 .with_isolation(ConnectionLevelIsolation::ConnectionBinding)
365 .with_affinity_policy(ConnectionAffinityPolicy::Strict)
366 .with_affinity_timeout_ms(10_000);
367 assert_eq!(
368 config.isolation,
369 ConnectionLevelIsolation::ConnectionBinding
370 );
371 assert_eq!(config.affinity_policy, ConnectionAffinityPolicy::Strict);
372 assert_eq!(config.affinity_timeout_ms, 10_000);
373 }
374
375 #[test]
376 fn test_isolation_serde() {
377 let isolations = vec![
378 ConnectionLevelIsolation::SetTenantId,
379 ConnectionLevelIsolation::SchemaIsolation,
380 ConnectionLevelIsolation::ConnectionBinding,
381 ];
382 for i in &isolations {
383 let json = serde_json::to_string(i).unwrap();
384 let decoded: ConnectionLevelIsolation = serde_json::from_str(&json).unwrap();
385 assert_eq!(*i, decoded);
386 }
387 }
388
389 #[test]
390 fn test_affinity_policy_serde() {
391 let policies = vec![
392 ConnectionAffinityPolicy::Strict,
393 ConnectionAffinityPolicy::Preferred,
394 ConnectionAffinityPolicy::None,
395 ];
396 for p in &policies {
397 let json = serde_json::to_string(p).unwrap();
398 let decoded: ConnectionAffinityPolicy = serde_json::from_str(&json).unwrap();
399 assert_eq!(*p, decoded);
400 }
401 }
402
403 #[test]
404 fn test_supports_set_tenant_id() {
405 let pool = make_pool();
406 let pg_config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
407 let pg_binder = ConnectionTenantBinder::new(pool.clone(), pg_config);
408 assert!(pg_binder.supports_set_tenant_id());
409
410 let mysql_config = ConnectionLevelTenantConfig::new(DbType::MySQL);
411 let mysql_binder = ConnectionTenantBinder::new(pool.clone(), mysql_config);
412 assert!(mysql_binder.supports_set_tenant_id());
413
414 let sqlite_config = ConnectionLevelTenantConfig::new(DbType::Sqlite);
415 let sqlite_binder = ConnectionTenantBinder::new(pool.clone(), sqlite_config);
416 assert!(!sqlite_binder.supports_set_tenant_id());
417
418 let oracle_config = ConnectionLevelTenantConfig::new(DbType::Oracle);
419 let oracle_binder = ConnectionTenantBinder::new(pool, oracle_config);
420 assert!(!oracle_binder.supports_set_tenant_id());
421 }
422
423 #[test]
424 fn test_build_set_tenant_sql() {
425 let pool = make_pool();
426 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
427 let binder = ConnectionTenantBinder::new(pool, config);
428 let sql = binder.build_set_tenant_sql("tenant_123");
429 assert!(sql.contains("SET app.tenant_id"));
430 assert!(sql.contains("tenant_123"));
431 }
432
433 #[test]
434 fn test_build_clear_tenant_sql() {
435 let pool = make_pool();
436 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
437 let binder = ConnectionTenantBinder::new(pool, config);
438 let sql = binder.build_clear_tenant_sql();
439 assert!(sql.contains("NULL"));
440 }
441
442 #[test]
443 fn test_bind_and_find_connection() {
444 let pool = make_pool();
445 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
446 let binder = ConnectionTenantBinder::new(pool, config);
447
448 let conn_id1 = binder.bind_connection("tenant_1");
449 let conn_id2 = binder.bind_connection("tenant_1");
450 let conn_id3 = binder.bind_connection("tenant_2");
451
452 assert_ne!(conn_id1, conn_id2);
453 assert_ne!(conn_id1, conn_id3);
454
455 let tenant1_conns = binder.find_bound_connections("tenant_1");
456 assert_eq!(tenant1_conns.len(), 2);
457 assert!(tenant1_conns.contains(&conn_id1));
458 assert!(tenant1_conns.contains(&conn_id2));
459
460 let tenant2_conns = binder.find_bound_connections("tenant_2");
461 assert_eq!(tenant2_conns.len(), 1);
462 assert!(tenant2_conns.contains(&conn_id3));
463
464 assert_eq!(binder.binding_count("tenant_1"), 2);
465 assert_eq!(binder.binding_count("tenant_2"), 1);
466 assert_eq!(binder.binding_count("tenant_3"), 0);
467 }
468
469 #[test]
470 fn test_unbind_connection() {
471 let pool = make_pool();
472 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
473 let binder = ConnectionTenantBinder::new(pool, config);
474
475 let conn_id1 = binder.bind_connection("tenant_1");
476 let conn_id2 = binder.bind_connection("tenant_1");
477 assert_eq!(binder.binding_count("tenant_1"), 2);
478
479 binder.unbind_connection("tenant_1", conn_id1);
480 assert_eq!(binder.binding_count("tenant_1"), 1);
481
482 let conns = binder.find_bound_connections("tenant_1");
483 assert!(conns.contains(&conn_id2));
484 assert!(!conns.contains(&conn_id1));
485
486 binder.unbind_connection("tenant_1", conn_id2);
487 assert_eq!(binder.binding_count("tenant_1"), 0);
488 }
489
490 #[test]
491 fn test_validate_tenant_id() {
492 let pool = make_pool();
493 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
494 let binder = ConnectionTenantBinder::new(pool, config);
495
496 assert!(binder.validate_tenant_id("tenant_1").is_ok());
497 assert_eq!(
498 binder.validate_tenant_id("").unwrap_err(),
499 TenantError::EmptyTenantId
500 );
501 }
502
503 #[test]
504 fn test_resolve_isolation_pg() {
505 let pool = make_pool();
506 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
507 let binder = ConnectionTenantBinder::new(pool, config);
508 assert_eq!(
509 binder.resolve_isolation(),
510 ConnectionLevelIsolation::SetTenantId
511 );
512 }
513
514 #[test]
515 fn test_resolve_isolation_sqlite_fallback() {
516 let pool = make_pool();
517 let config = ConnectionLevelTenantConfig::new(DbType::Sqlite);
518 let binder = ConnectionTenantBinder::new(pool, config);
519 assert_eq!(
520 binder.resolve_isolation(),
521 ConnectionLevelIsolation::SchemaIsolation
522 );
523 }
524
525 #[test]
526 fn test_resolve_isolation_schema_no_fallback() {
527 let pool = make_pool();
528 let config = ConnectionLevelTenantConfig::new(DbType::Sqlite)
529 .with_isolation(ConnectionLevelIsolation::SchemaIsolation);
530 let binder = ConnectionTenantBinder::new(pool, config);
531 assert_eq!(
532 binder.resolve_isolation(),
533 ConnectionLevelIsolation::SchemaIsolation
534 );
535 }
536
537 #[test]
538 fn test_tenant_error_display() {
539 let err = TenantError::NoBoundConnection;
540 assert!(err.to_string().contains("no connection"));
541 let err = TenantError::EmptyTenantId;
542 assert!(err.to_string().contains("empty"));
543 let err = TenantError::UnsupportedDialect;
544 assert!(err.to_string().contains("unsupported"));
545 }
546
547 #[test]
548 fn test_guard_drop_unbinds() {
549 let pool = make_pool();
550 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
551 let binder = Arc::new(ConnectionTenantBinder::new(pool, config));
552
553 let conn_id = binder.bind_connection("tenant_1");
554 assert_eq!(binder.binding_count("tenant_1"), 1);
555
556 {
557 let _guard =
558 TenantConnectionGuard::new(binder.clone(), "tenant_1".to_string(), conn_id);
559 assert_eq!(binder.binding_count("tenant_1"), 1);
560 }
561
562 assert_eq!(binder.binding_count("tenant_1"), 0);
563 }
564
565 #[test]
566 fn test_guard_release() {
567 let pool = make_pool();
568 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
569 let binder = Arc::new(ConnectionTenantBinder::new(pool, config));
570
571 let conn_id = binder.bind_connection("tenant_1");
572 let mut guard = TenantConnectionGuard::new(binder, "tenant_1".to_string(), conn_id);
573 assert!(guard.is_active());
574 guard.release().unwrap();
575 assert!(!guard.is_active());
576 }
577
578 #[test]
579 fn test_guard_properties() {
580 let pool = make_pool();
581 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
582 let binder = Arc::new(ConnectionTenantBinder::new(pool, config));
583
584 let guard = TenantConnectionGuard::new(binder, "tenant_42".to_string(), 999);
585 assert_eq!(guard.tenant_id(), "tenant_42");
586 assert_eq!(guard.connection_id(), 999);
587 assert!(guard.is_active());
588 assert!(guard.clear_tenant_sql().contains("NULL"));
589 }
590
591 #[test]
592 fn test_all_bindings() {
593 let pool = make_pool();
594 let config = ConnectionLevelTenantConfig::new(DbType::PostgreSQL);
595 let binder = ConnectionTenantBinder::new(pool, config);
596
597 binder.bind_connection("tenant_1");
598 binder.bind_connection("tenant_1");
599 binder.bind_connection("tenant_2");
600
601 let all = binder.all_bindings();
602 assert_eq!(all.len(), 3);
603 }
604}