From 7c4cb70047f1ac97aff61ca2faa4a6edc0b86102 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Fri, 10 Apr 2026 12:59:40 +0200 Subject: [PATCH] Reject mixed wildcard SQL projections without panicking --- src/planner/sql.rs | 52 +++++++++++++++++++++++++++++++++++++++++----- src/sql/parser.rs | 21 +++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/planner/sql.rs b/src/planner/sql.rs index d7c5846..7151b06 100644 --- a/src/planner/sql.rs +++ b/src/planner/sql.rs @@ -22,6 +22,8 @@ pub enum PlannerError { DuplicateSourceName(String), /// The current `ORDER BY` subset only supports output column names. UnsupportedOrderBy, + /// The parser or AST contains a wildcard mixed with other projection items. + MixedWildcardProjection, } impl fmt::Display for PlannerError { @@ -35,6 +37,12 @@ impl fmt::Display for PlannerError { Self::UnsupportedOrderBy => { 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)> { match self { Self::Catalog(err) => Some(err), - Self::UnknownColumn(_) | Self::DuplicateSourceName(_) | Self::UnsupportedOrderBy => { - None - } + Self::UnknownColumn(_) + | Self::DuplicateSourceName(_) + | Self::UnsupportedOrderBy + | Self::MixedWildcardProjection => None, } } } @@ -92,7 +101,7 @@ pub fn plan_select( }); 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 crate::catalog::PredicateCatalog; use crate::chase::{Atom, Instance, Term}; - use crate::sql::parser::parse_select; + use crate::sql::parser::{ParseError, parse_select}; #[test] fn plans_projection_and_filter() { @@ -466,4 +475,37 @@ mod tests { 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" + ); + } } diff --git a/src/sql/parser.rs b/src/sql/parser.rs index c127dfa..d6add09 100644 --- a/src/sql/parser.rs +++ b/src/sql/parser.rs @@ -12,6 +12,7 @@ pub enum ParseError { ExpectedToken(&'static str), ExpectedIdentifier, UnexpectedToken(String), + MixedWildcardProjection, UnterminatedString, } @@ -22,6 +23,12 @@ impl fmt::Display for ParseError { Self::ExpectedToken(token) => write!(f, "expected `{}`", token), Self::ExpectedIdentifier => write!(f, "expected identifier"), 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"), } } @@ -125,6 +132,14 @@ impl Parser { break; } + if items.len() > 1 + && items + .iter() + .any(|item| matches!(item, SelectItem::Wildcard)) + { + return Err(ParseError::MixedWildcardProjection); + } + Ok(items) } @@ -515,4 +530,10 @@ mod tests { assert_eq!(select.from.len(), 1); 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); + } }