use std::collections::HashMap;
pub trait TableMeta {
fn table_name() -> &'static str;
fn schema_name() -> &'static str {
"public"
}
fn columns() -> &'static [&'static str];
fn primary_key() -> Option<&'static str> {
None
}
}
#[derive(Debug, Clone)]
pub struct ColumnMeta {
pub name: String,
pub is_primary_key: bool,
}
#[derive(Debug, Clone)]
pub struct TableSchema {
pub schema: String,
pub name: String,
pub columns: Vec<ColumnMeta>,
}
impl TableSchema {
pub fn new(schema: impl Into<String>, name: impl Into<String>) -> Self {
Self {
schema: schema.into(),
name: name.into(),
columns: Vec::new(),
}
}
pub fn add_column(&mut self, name: impl Into<String>, is_primary_key: bool) {
self.columns.push(ColumnMeta {
name: name.into(),
is_primary_key,
});
}
pub fn with_columns(mut self, columns: &[&str]) -> Self {
for col in columns {
self.columns.push(ColumnMeta {
name: col.to_string(),
is_primary_key: false,
});
}
self
}
pub fn with_primary_key(mut self, pk: &str) -> Self {
for col in &mut self.columns {
col.is_primary_key = col.name == pk;
}
if !self.columns.iter().any(|c| c.name == pk) {
self.columns.push(ColumnMeta {
name: pk.to_string(),
is_primary_key: true,
});
}
self
}
pub fn has_column(&self, name: &str) -> bool {
self.columns.iter().any(|c| c.name == name)
}
}
#[derive(Debug, Clone)]
pub struct SchemaRegistry {
pub(super) tables: HashMap<String, HashMap<String, TableSchema>>,
#[cfg(feature = "check")]
pub(super) parse_cache: std::sync::Arc<pgorm_check::SqlParseCache>,
}
impl SchemaRegistry {
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "check")]
pub fn with_parse_cache_capacity(capacity: usize) -> Self {
Self {
tables: HashMap::new(),
parse_cache: std::sync::Arc::new(pgorm_check::SqlParseCache::new(capacity)),
}
}
pub fn register<T: TableMeta>(&mut self) {
let schema_name = T::schema_name().to_string();
let table_name = T::table_name().to_string();
let columns = T::columns();
let pk = T::primary_key();
let mut table = TableSchema::new(&schema_name, &table_name);
for col in columns {
let is_pk = pk == Some(*col);
table.add_column(*col, is_pk);
}
self.tables
.entry(schema_name)
.or_default()
.insert(table_name, table);
}
pub fn register_table(&mut self, table: TableSchema) {
let schema = table.schema.clone();
let name = table.name.clone();
self.tables.entry(schema).or_default().insert(name, table);
}
pub fn get_table(&self, schema: &str, name: &str) -> Option<&TableSchema> {
self.tables
.get(schema)
.and_then(|by_name| by_name.get(name))
}
pub fn find_table(&self, name: &str) -> Option<&TableSchema> {
if let Some(t) = self.get_table("public", name) {
return Some(t);
}
let mut matches: Vec<_> = self
.tables
.iter()
.filter(|(schema, _)| schema.as_str() != "public")
.filter_map(|(schema, by_name)| by_name.get(name).map(|t| (schema.as_str(), t)))
.collect();
matches.sort_by_key(|(schema, _)| *schema);
if matches.len() > 1 {
let schemas: Vec<_> = matches.iter().map(|(s, _)| *s).collect();
crate::error::pgorm_warn(&format!(
"Ambiguous table '{}' found in schemas: {:?}. Using '{}'. \
Use get_table(schema, name) for explicit lookup.",
name, schemas, schemas[0]
));
}
matches.into_iter().next().map(|(_, t)| t)
}
pub fn has_table(&self, schema: &str, name: &str) -> bool {
self.tables
.get(schema)
.is_some_and(|by_name| by_name.contains_key(name))
}
pub fn tables(&self) -> impl Iterator<Item = &TableSchema> {
self.tables.values().flat_map(|by_name| by_name.values())
}
pub fn len(&self) -> usize {
self.tables.values().map(|by_name| by_name.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.tables.values().all(|by_name| by_name.is_empty())
}
}
impl Default for SchemaRegistry {
fn default() -> Self {
Self {
tables: HashMap::new(),
#[cfg(feature = "check")]
parse_cache: std::sync::Arc::new(pgorm_check::SqlParseCache::default()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaIssueLevel {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaIssueKind {
ParseError,
MissingTable,
MissingColumn,
AmbiguousColumn,
Unsupported,
}
#[derive(Debug, Clone)]
pub struct SchemaIssue {
pub level: SchemaIssueLevel,
pub kind: SchemaIssueKind,
pub message: String,
}
impl std::fmt::Display for SchemaIssue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?} {:?}: {}", self.level, self.kind, self.message)
}
}