use nodedb_types::DatabaseId;
use sqlparser::ast::{ObjectName, TableFactor, TableWithJoins};
use crate::catalog::SqlCatalog;
use crate::types::{CollectionInfo, ColumnInfo};
struct Relation {
qualifier: String,
info: CollectionInfo,
}
pub(super) struct Scope {
relations: Vec<Relation>,
opaque: bool,
}
impl Scope {
pub(super) fn from_tables(catalog: &dyn SqlCatalog, tables: &[TableWithJoins]) -> Self {
let mut scope = Self {
relations: Vec::new(),
opaque: false,
};
scope.add_tables(catalog, tables);
scope
}
pub(super) fn add_tables(&mut self, catalog: &dyn SqlCatalog, tables: &[TableWithJoins]) {
for table in tables {
self.add_factor(catalog, &table.relation);
for join in &table.joins {
self.add_factor(catalog, &join.relation);
}
}
}
fn add_factor(&mut self, catalog: &dyn SqlCatalog, factor: &TableFactor) {
let TableFactor::Table {
name, alias, args, ..
} = factor
else {
self.opaque = true;
return;
};
if args.is_some() {
self.opaque = true;
return;
}
let Some(relation_name) = last_ident(name) else {
self.opaque = true;
return;
};
let info = match catalog.resolve_relation(DatabaseId::DEFAULT, &relation_name) {
Ok(Some(info)) => info,
Ok(None) | Err(_) => {
self.opaque = true;
return;
}
};
let qualifier = alias
.as_ref()
.map(|alias| alias.name.value.clone())
.unwrap_or(relation_name);
self.relations.push(Relation { qualifier, info });
}
pub(super) fn resolve_column(&self, parts: &[&str]) -> Option<&ColumnInfo> {
match parts {
[column] => self.resolve_bare(column),
[qualifier, column] => self.resolve_qualified(qualifier, column),
_ => None,
}
}
fn resolve_bare(&self, column: &str) -> Option<&ColumnInfo> {
if self.opaque {
return None;
}
let mut found: Option<&ColumnInfo> = None;
for relation in &self.relations {
let Some(candidate) = column_of(&relation.info, column) else {
continue;
};
if found.is_some() {
return None;
}
found = Some(candidate);
}
found
}
fn resolve_qualified(&self, qualifier: &str, column: &str) -> Option<&ColumnInfo> {
let mut matching = self
.relations
.iter()
.filter(|relation| relation.qualifier.eq_ignore_ascii_case(qualifier));
let relation = matching.next()?;
if matching.next().is_some() {
return None;
}
column_of(&relation.info, column)
}
}
pub(super) fn lookup_relation(
catalog: &dyn SqlCatalog,
name: &ObjectName,
) -> Option<CollectionInfo> {
let relation_name = last_ident(name)?;
match catalog.resolve_relation(DatabaseId::DEFAULT, &relation_name) {
Ok(Some(info)) => Some(info),
Ok(None) | Err(_) => None,
}
}
pub(super) fn column_of<'a>(info: &'a CollectionInfo, column: &str) -> Option<&'a ColumnInfo> {
info.columns
.iter()
.find(|candidate| candidate.name.eq_ignore_ascii_case(column))
}
fn last_ident(name: &ObjectName) -> Option<String> {
name.0
.last()
.and_then(|part| part.as_ident())
.map(|ident| ident.value.clone())
}