use crate::casing::IdentifierCasing;
use crate::error::Error;
use sqlparser::ast::{Ident, Statement};
use sqlparser::dialect::Dialect;
use sqlparser::parser::Parser;
use std::fmt;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Catalog {
tables: Vec<CatalogTable>,
default_catalog: Option<String>,
default_schema: Option<String>,
}
impl Catalog {
pub fn new() -> Self {
Self::default()
}
pub fn table(mut self, table: CatalogTable) -> Self {
self.tables.push(table);
self
}
pub fn default_catalog(mut self, catalog: impl Into<String>) -> Self {
self.default_catalog = Some(catalog.into());
self
}
pub fn default_schema(mut self, schema: impl Into<String>) -> Self {
self.default_schema = Some(schema.into());
self
}
pub fn from_ddl(dialect: &dyn Dialect, ddl: &str) -> Result<Self, Error> {
Self::from_ddl_with_casing(dialect, ddl, IdentifierCasing::for_dialect(dialect))
}
pub fn from_ddl_with_casing(
dialect: &dyn Dialect,
ddl: &str,
casing: IdentifierCasing,
) -> Result<Self, Error> {
let statements = Parser::parse_sql(dialect, ddl)?;
let mut catalog = Catalog::new();
for statement in &statements {
let Statement::CreateTable(create) = statement else {
continue;
};
if create.columns.is_empty() {
continue;
}
let Some(parts) = create
.name
.0
.iter()
.map(|p| p.as_ident())
.collect::<Option<Vec<&Ident>>>()
else {
continue;
};
let name = |id: &Ident| casing.table.normalize(id);
let table = match parts.as_slice() {
[n] => CatalogTable::unqualified(name(n)),
[schema, n] => CatalogTable::new(name(schema), name(n)),
[catalog_seg, schema, n] => {
CatalogTable::new(name(schema), name(n)).catalog(name(catalog_seg))
}
_ => continue,
};
let columns = create
.columns
.iter()
.map(|c| casing.column.normalize(&c.name));
catalog = catalog.table(table.columns(columns));
}
Ok(catalog)
}
pub(crate) fn tables(&self) -> &[CatalogTable] {
&self.tables
}
pub(crate) fn default_catalog_segment(&self) -> Option<&str> {
self.default_catalog.as_deref()
}
pub(crate) fn default_schema_segment(&self) -> Option<&str> {
self.default_schema.as_deref()
}
}
impl FromIterator<CatalogTable> for Catalog {
fn from_iter<I: IntoIterator<Item = CatalogTable>>(iter: I) -> Self {
Self {
tables: iter.into_iter().collect(),
default_catalog: None,
default_schema: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CatalogTable {
catalog: Option<String>,
schema: Option<String>,
name: String,
columns: Vec<String>,
}
impl CatalogTable {
pub fn new(schema: impl Into<String>, name: impl Into<String>) -> Self {
Self {
catalog: None,
schema: Some(schema.into()),
name: name.into(),
columns: Vec::new(),
}
}
pub fn unqualified(name: impl Into<String>) -> Self {
Self {
catalog: None,
schema: None,
name: name.into(),
columns: Vec::new(),
}
}
pub fn catalog(mut self, catalog: impl Into<String>) -> Self {
self.catalog = Some(catalog.into());
self
}
pub fn columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.columns = columns.into_iter().map(Into::into).collect();
self
}
pub(crate) fn catalog_segment(&self) -> Option<&str> {
self.catalog.as_deref()
}
pub(crate) fn schema_segment(&self) -> Option<&str> {
self.schema.as_deref()
}
pub(crate) fn name_segment(&self) -> &str {
&self.name
}
pub(crate) fn column_names(&self) -> &[String] {
&self.columns
}
}
impl fmt::Display for CatalogTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path = [
self.catalog.as_deref(),
self.schema.as_deref(),
Some(self.name.as_str()),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(".");
write!(f, "{path}")
}
}