Reject mixed wildcard SQL projections without panicking

This commit is contained in:
Hassan Abedi 2026-04-10 12:59:40 +02:00
parent 7111a682ff
commit 7c4cb70047
2 changed files with 68 additions and 5 deletions

View File

@ -22,6 +22,8 @@ pub enum PlannerError {
DuplicateSourceName(String), DuplicateSourceName(String),
/// The current `ORDER BY` subset only supports output column names. /// The current `ORDER BY` subset only supports output column names.
UnsupportedOrderBy, UnsupportedOrderBy,
/// The parser or AST contains a wildcard mixed with other projection items.
MixedWildcardProjection,
} }
impl fmt::Display for PlannerError { impl fmt::Display for PlannerError {
@ -35,6 +37,12 @@ impl fmt::Display for PlannerError {
Self::UnsupportedOrderBy => { Self::UnsupportedOrderBy => {
write!(f, "only output column names are supported in ORDER BY") write!(f, "only output column names are supported in ORDER BY")
} }
Self::MixedWildcardProjection => {
write!(
f,
"wildcard projections cannot be combined with other items"
)
}
} }
} }
} }
@ -43,9 +51,10 @@ impl Error for PlannerError {
fn source(&self) -> Option<&(dyn Error + 'static)> { fn source(&self) -> Option<&(dyn Error + 'static)> {
match self { match self {
Self::Catalog(err) => Some(err), Self::Catalog(err) => Some(err),
Self::UnknownColumn(_) | Self::DuplicateSourceName(_) | Self::UnsupportedOrderBy => { Self::UnknownColumn(_)
None | Self::DuplicateSourceName(_)
} | Self::UnsupportedOrderBy
| Self::MixedWildcardProjection => None,
} }
} }
} }
@ -92,7 +101,7 @@ pub fn plan_select(
}); });
fields.push(Field::new(output_name, data_type, nullable)); fields.push(Field::new(output_name, data_type, nullable));
} }
SelectItem::Wildcard => unreachable!("wildcard projections are handled earlier"), SelectItem::Wildcard => return Err(PlannerError::MixedWildcardProjection),
} }
} }
@ -287,7 +296,7 @@ mod tests {
use super::*; use super::*;
use crate::catalog::PredicateCatalog; use crate::catalog::PredicateCatalog;
use crate::chase::{Atom, Instance, Term}; use crate::chase::{Atom, Instance, Term};
use crate::sql::parser::parse_select; use crate::sql::parser::{ParseError, parse_select};
#[test] #[test]
fn plans_projection_and_filter() { fn plans_projection_and_filter() {
@ -466,4 +475,37 @@ mod tests {
other => panic!("unexpected plan: {:?}", other), other => panic!("unexpected plan: {:?}", other),
} }
} }
#[test]
fn rejects_mixed_wildcard_projection() {
let instance: Instance = vec![Atom::new(
"Parent",
vec![Term::constant("alice"), Term::constant("bob")],
)]
.into_iter()
.collect();
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
let select = parse_select("SELECT *, c0 FROM Parent").unwrap_err();
assert_eq!(select, ParseError::MixedWildcardProjection);
let malformed = Select {
projection: vec![
SelectItem::Wildcard,
SelectItem::Expr {
expr: Expr::Identifier("c0".to_string()),
alias: None,
},
],
from: vec![TableRef {
name: "Parent".to_string(),
alias: None,
}],
selection: None,
order_by: Vec::new(),
};
let error = plan_select(&malformed, &catalog).unwrap_err();
assert_eq!(
error.to_string(),
"wildcard projections cannot be combined with other items"
);
}
} }

View File

@ -12,6 +12,7 @@ pub enum ParseError {
ExpectedToken(&'static str), ExpectedToken(&'static str),
ExpectedIdentifier, ExpectedIdentifier,
UnexpectedToken(String), UnexpectedToken(String),
MixedWildcardProjection,
UnterminatedString, UnterminatedString,
} }
@ -22,6 +23,12 @@ impl fmt::Display for ParseError {
Self::ExpectedToken(token) => write!(f, "expected `{}`", token), Self::ExpectedToken(token) => write!(f, "expected `{}`", token),
Self::ExpectedIdentifier => write!(f, "expected identifier"), Self::ExpectedIdentifier => write!(f, "expected identifier"),
Self::UnexpectedToken(token) => write!(f, "unexpected token `{}`", token), Self::UnexpectedToken(token) => write!(f, "unexpected token `{}`", token),
Self::MixedWildcardProjection => {
write!(
f,
"wildcard projections cannot be combined with other items"
)
}
Self::UnterminatedString => write!(f, "unterminated string literal"), Self::UnterminatedString => write!(f, "unterminated string literal"),
} }
} }
@ -125,6 +132,14 @@ impl Parser {
break; break;
} }
if items.len() > 1
&& items
.iter()
.any(|item| matches!(item, SelectItem::Wildcard))
{
return Err(ParseError::MixedWildcardProjection);
}
Ok(items) Ok(items)
} }
@ -515,4 +530,10 @@ mod tests {
assert_eq!(select.from.len(), 1); assert_eq!(select.from.len(), 1);
assert_eq!(select.from[0].name, "Employee-Records:2025"); assert_eq!(select.from[0].name, "Employee-Records:2025");
} }
#[test]
fn rejects_mixed_wildcard_projection() {
let error = parse_select("SELECT *, c0 FROM Parent").unwrap_err();
assert_eq!(error, ParseError::MixedWildcardProjection);
}
} }