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::{
JsonPrimitive, SchemaDict,
database::{FieldInfo, SchemaProvider},
parser::ast::{Column, ScalarExpr},
};
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"));
}
}