use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::RwLock;
#[derive(Debug, Default)]
pub struct TenantRegistry {
tables: HashMap<String, String>,
}
impl TenantRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, table: impl Into<String>, column: impl Into<String>) {
self.tables.insert(table.into(), column.into());
}
pub fn get(&self, table: &str) -> Option<&str> {
self.tables.get(table).map(|s| s.as_str())
}
pub fn is_tenant_table(&self, table: &str) -> bool {
self.tables.contains_key(table)
}
pub fn len(&self) -> usize {
self.tables.len()
}
pub fn is_empty(&self) -> bool {
self.tables.is_empty()
}
pub fn tables(&self) -> impl Iterator<Item = (&str, &str)> {
self.tables.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
pub fn from_build_schema(schema: &crate::build::Schema) -> Self {
let mut registry = Self::new();
for table in schema.tables.values() {
if table.columns.contains_key("tenant_id") {
registry.register(&table.name, "tenant_id");
}
}
registry
}
}
pub static TENANT_TABLES: LazyLock<RwLock<TenantRegistry>> =
LazyLock::new(|| RwLock::new(TenantRegistry::new()));
pub fn register_tenant_table(table: &str, column: &str) {
if let Ok(mut reg) = TENANT_TABLES.write() {
reg.register(table, column);
}
}
pub fn lookup_tenant_column(table: &str) -> Option<String> {
let registry = TENANT_TABLES.read().ok()?;
registry.get(table).map(|s| s.to_string())
}
pub fn load_tenant_tables(path: &str) -> Result<usize, String> {
let schema = crate::build::Schema::parse_file(path)?;
let mut registry = TENANT_TABLES
.write()
.map_err(|e| format!("Lock error: {}", e))?;
let mut count = 0;
for table in schema.tables.values() {
if table.columns.contains_key("tenant_id") {
registry.register(&table.name, "tenant_id");
count += 1;
}
}
Ok(count)
}
pub fn register_tenant_tables(tables: &[(&str, &str)]) {
if let Ok(mut reg) = TENANT_TABLES.write() {
for (table, column) in tables {
reg.register(*table, *column);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
fn registry_test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.expect("tenant registry test mutex poisoned")
}
fn clear_global_registry() {
if let Ok(mut reg) = TENANT_TABLES.write() {
*reg = TenantRegistry::new();
}
}
#[test]
fn test_registry_register_and_lookup() {
let mut reg = TenantRegistry::new();
reg.register("orders", "tenant_id");
reg.register("bookings", "tenant_id");
assert_eq!(reg.get("orders"), Some("tenant_id"));
assert_eq!(reg.get("bookings"), Some("tenant_id"));
assert_eq!(reg.get("migrations"), None);
}
#[test]
fn test_registry_is_tenant_table() {
let mut reg = TenantRegistry::new();
reg.register("orders", "tenant_id");
assert!(reg.is_tenant_table("orders"));
assert!(!reg.is_tenant_table("users"));
}
#[test]
fn test_registry_len() {
let mut reg = TenantRegistry::new();
assert!(reg.is_empty());
reg.register("orders", "tenant_id");
assert_eq!(reg.len(), 1);
assert!(!reg.is_empty());
}
#[test]
fn test_global_register_and_lookup() {
let _lock = registry_test_lock();
clear_global_registry();
register_tenant_table("_test_t1", "tenant_id");
assert_eq!(
lookup_tenant_column("_test_t1"),
Some("tenant_id".to_string())
);
assert_eq!(lookup_tenant_column("_test_nonexistent"), None);
clear_global_registry();
}
#[test]
fn test_bulk_register() {
let _lock = registry_test_lock();
clear_global_registry();
register_tenant_tables(&[("_test_bulk_a", "tenant_id"), ("_test_bulk_b", "tenant_id")]);
assert_eq!(
lookup_tenant_column("_test_bulk_a"),
Some("tenant_id".to_string())
);
assert_eq!(
lookup_tenant_column("_test_bulk_b"),
Some("tenant_id".to_string())
);
clear_global_registry();
}
#[test]
fn test_from_build_schema_prefers_tenant_id() {
let schema = crate::build::Schema::parse(
r#"
table orders {
id UUID
tenant_id UUID
}
"#,
)
.expect("schema should parse");
let reg = TenantRegistry::from_build_schema(&schema);
assert_eq!(reg.get("orders"), Some("tenant_id"));
assert_eq!(reg.get("legacy_bookings"), None);
}
}