use crate::parser::{analyzer::{AnalysisContext, AnalyzerError, WildcardResolver}, ast::Identifier};
pub struct IdentifierResolver;
impl IdentifierResolver {
pub fn expand_projection_idents(proj: &[Identifier], ctx: &AnalysisContext) -> Result<Vec<Identifier>, AnalyzerError> {
let mut result = Vec::new();
for id in proj {
let expr = WildcardResolver::expand_wildcard(&id.expression, ctx)?;
if expr.len() == 1 {
result.push(Identifier { expression: expr.into_iter().next().unwrap(), alias: id.alias.clone() });
} else {
for e in expr {
result.push(Identifier { expression: e, alias: None });
}
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use crate::{database::{FieldInfo, SchemaProvider}, parser::ast::{Column, ScalarExpr}, JsonPrimitive, SchemaDict};
use super::*;
use indexmap::IndexMap;
struct DummySchemas {
by_name: std::collections::HashMap<String, SchemaDict>,
}
impl DummySchemas {
fn new() -> Self { Self { by_name: std::collections::HashMap::new() } }
fn with(mut self, name: &str, fields: Vec<(&str, JsonPrimitive, bool)>) -> Self {
let mut m = IndexMap::new();
for (k, ty, nullable) in fields {
m.insert(k.to_string(), FieldInfo { ty, nullable });
}
self.by_name.insert(name.to_string(), SchemaDict { fields: m });
self
}
}
impl SchemaProvider for DummySchemas {
fn schema_of(&self, backing_collection: &str) -> Option<SchemaDict> {
self.by_name.get(backing_collection).cloned()
}
}
fn ctx_with_tables<'a>(sp: &'a DummySchemas, pairs: &'a [(&'a str, Option<&'a str>)]) -> AnalysisContext<'a> {
let mut ctx = AnalysisContext::new(sp);
for (name, alias) in pairs {
let visible = alias.unwrap_or(name).to_string();
ctx.add_collection(visible, (*name).to_string());
}
ctx
}
fn ident(expr: ScalarExpr, alias: Option<&str>) -> Identifier {
Identifier { expression: expr, alias: alias.map(|s| s.to_string()) }
}
#[test]
fn no_wildcard_preserves_alias() {
let sp = DummySchemas::new().with("t", vec![
("a", JsonPrimitive::Int, false),
]);
let ctx = ctx_with_tables(&sp, &[("t", None)]);
let proj = vec![ ident(ScalarExpr::Column(Column::Name { name: "a".into() }), Some("aa")) ];
let out = IdentifierResolver::expand_projection_idents(&proj, &ctx).expect("expand");
assert_eq!(out.len(), 1);
assert_eq!(out[0].alias.as_deref(), Some("aa"));
assert!(matches!(out[0].expression, ScalarExpr::Column(Column::Name { ref name }) if name == "a"));
}
#[test]
fn star_expands_all_visible_collections_in_order_and_drops_aliases() {
let sp = DummySchemas::new()
.with("t1", vec![("id", JsonPrimitive::Int, false), ("name", JsonPrimitive::String, false)])
.with("t2", vec![("x", JsonPrimitive::Int, false)]);
let ctx = ctx_with_tables(&sp, &[("t1", None), ("t2", None)]);
let proj = vec![ ident(ScalarExpr::WildCard, Some("ignored")), ];
let out = IdentifierResolver::expand_projection_idents(&proj, &ctx).expect("expand");
let cols: Vec<(String,String,Option<String>)> = out.into_iter().map(|id| {
let alias = id.alias;
match id.expression {
ScalarExpr::Column(Column::WithCollection{ collection, name }) => (collection, name, alias),
other => panic!("expected qualified column after wildcard expand, got {other:?}"),
}
}).collect();
assert_eq!(cols, vec![
("t1".into(), "id".into(), None),
("t1".into(), "name".into(), None),
("t2".into(), "x".into(), None),
]);
}
#[test]
fn table_star_expands_only_that_collection_and_keeps_alias_when_single_column() {
let sp = DummySchemas::new()
.with("t1", vec![("only", JsonPrimitive::Int, false)])
.with("t2", vec![("a", JsonPrimitive::Int, false), ("b", JsonPrimitive::Int, false)]);
let ctx = ctx_with_tables(&sp, &[("t1", None), ("t2", None)]);
let proj = vec![ ident(ScalarExpr::WildCardWithCollection("t1".into()), Some("alias")) ];
let out = IdentifierResolver::expand_projection_idents(&proj, &ctx).expect("expand");
assert_eq!(out.len(), 1);
assert_eq!(out[0].alias.as_deref(), Some("alias"));
match &out[0].expression {
ScalarExpr::Column(Column::WithCollection{ collection, name }) => {
assert_eq!(collection, "t1");
assert_eq!(name, "only");
}
other => panic!("expected t1.only after table star, got {other:?}"),
}
let proj2 = vec![ ident(ScalarExpr::WildCardWithCollection("t2".into()), Some("dropme")) ];
let out2 = IdentifierResolver::expand_projection_idents(&proj2, &ctx).expect("expand");
assert_eq!(out2.len(), 2);
assert!(out2.iter().all(|id| id.alias.is_none()));
let names: Vec<_> = out2.iter().map(|id| match &id.expression {
ScalarExpr::Column(Column::WithCollection{ collection, name }) => (collection.clone(), name.clone()),
other => panic!("expected column, got {other:?}"),
}).collect();
assert_eq!(names, vec![("t2".into(), "a".into()), ("t2".into(), "b".into())]);
}
#[test]
fn mixed_projection_expands_in_place_order() {
let sp = DummySchemas::new()
.with("t", vec![("a", JsonPrimitive::Int, false), ("b", JsonPrimitive::Int, false)]);
let ctx = ctx_with_tables(&sp, &[("t", None)]);
let proj = vec![
ident(ScalarExpr::Column(Column::Name { name: "a".into() }), Some("aa")),
ident(ScalarExpr::WildCardWithCollection("t".into()), None),
ident(ScalarExpr::Column(Column::Name { name: "b".into() }), None),
];
let out = IdentifierResolver::expand_projection_idents(&proj, &ctx).expect("expand");
assert_eq!(out.len(), 4);
assert_eq!(out[0].alias.as_deref(), Some("aa"));
assert!(matches!(out[0].expression, ScalarExpr::Column(Column::Name{ ref name }) if name=="a"));
for (i, nm) in ["a","b"].into_iter().enumerate() {
match &out[1+i].expression {
ScalarExpr::Column(Column::WithCollection{ collection, name }) => {
assert_eq!(collection, "t");
assert_eq!(name, nm);
}
other => panic!("expected qualified t.{nm}, got {other:?}"),
}
assert!(out[1+i].alias.is_none());
}
assert!(matches!(out[3].expression, ScalarExpr::Column(Column::Name{ ref name }) if name=="b"));
}
#[test]
fn table_star_unknown_visible_collection_errors() {
let sp = DummySchemas::new().with("t", vec![("a", JsonPrimitive::Int, false)]);
let ctx = ctx_with_tables(&sp, &[("t", None)]);
let proj = vec![ ident(ScalarExpr::WildCardWithCollection("v".into()), None) ];
let err = IdentifierResolver::expand_projection_idents(&proj, &ctx);
assert!(matches!(err, Err(AnalyzerError::UnknownCollection(c)) if c == "v"));
}
}