diff --git a/src/ast/query.rs b/src/ast/query.rs index 159f02a6c..440928ed7 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1018,13 +1018,13 @@ pub enum ExcludeSelectItem { /// ```plaintext /// /// ``` - Single(Ident), + Single(ObjectName), /// Multiple column names inside parenthesis. /// # Syntax /// ```plaintext /// (, , ...) /// ``` - Multiple(Vec), + Multiple(Vec), } impl fmt::Display for ExcludeSelectItem { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0b95c3ed7..43005cfbb 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1849,8 +1849,8 @@ impl Spanned for IlikeSelectItem { impl Spanned for ExcludeSelectItem { fn span(&self) -> Span { match self { - ExcludeSelectItem::Single(ident) => ident.span, - ExcludeSelectItem::Multiple(vec) => union_spans(vec.iter().map(|i| i.span)), + ExcludeSelectItem::Single(name) => name.span(), + ExcludeSelectItem::Multiple(vec) => union_spans(vec.iter().map(|i| i.span())), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8d8b55a34..bc91213fa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17951,11 +17951,12 @@ impl<'a> Parser<'a> { ) -> Result, ParserError> { let opt_exclude = if self.parse_keyword(Keyword::EXCLUDE) { if self.consume_token(&Token::LParen) { - let columns = self.parse_comma_separated(|parser| parser.parse_identifier())?; + let columns = + self.parse_comma_separated(|parser| parser.parse_object_name(false))?; self.expect_token(&Token::RParen)?; Some(ExcludeSelectItem::Multiple(columns)) } else { - let column = self.parse_identifier()?; + let column = self.parse_object_name(false)?; Some(ExcludeSelectItem::Single(column)) } } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8de460d78..ad7697a7f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17343,7 +17343,9 @@ fn test_select_exclude() { SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { assert_eq!( *opt_exclude, - Some(ExcludeSelectItem::Single(Ident::new("c1"))) + Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new( + "c1" + )))) ); } _ => unreachable!(), @@ -17356,8 +17358,8 @@ fn test_select_exclude() { assert_eq!( *opt_exclude, Some(ExcludeSelectItem::Multiple(vec![ - Ident::new("c1"), - Ident::new("c2") + ObjectName::from(Ident::new("c1")), + ObjectName::from(Ident::new("c2")), ])) ); } @@ -17368,7 +17370,9 @@ fn test_select_exclude() { SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { assert_eq!( *opt_exclude, - Some(ExcludeSelectItem::Single(Ident::new("c1"))) + Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new( + "c1" + )))) ); } _ => unreachable!(), @@ -17390,7 +17394,9 @@ fn test_select_exclude() { } assert_eq!( select.exclude, - Some(ExcludeSelectItem::Single(Ident::new("c1"))) + Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new( + "c1" + )))) ); let dialects = all_dialects_where(|d| { @@ -17401,7 +17407,9 @@ fn test_select_exclude() { SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { assert_eq!( *opt_exclude, - Some(ExcludeSelectItem::Single(Ident::new("c1"))) + Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new( + "c1" + )))) ); } _ => unreachable!(), @@ -17438,6 +17446,32 @@ fn test_select_exclude() { ); } +#[test] +fn test_select_exclude_qualified_names() { + // EXCLUDE should accept qualified names like `f.col` parsed as ObjectName. + let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude()); + + // Qualified name in multi-column EXCLUDE list: f.* EXCLUDE (f.col1, f.col2) + let select = dialects + .verified_only_select("SELECT f.* EXCLUDE (f.account_canonical_id, f.amount) FROM t AS f"); + match &select.projection[0] { + SelectItem::QualifiedWildcard(_, WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Multiple(vec![ + ObjectName::from(vec![Ident::new("f"), Ident::new("account_canonical_id")]), + ObjectName::from(vec![Ident::new("f"), Ident::new("amount")]), + ])) + ); + } + _ => unreachable!(), + } + + // Plain identifiers must still parse successfully. + dialects.verified_only_select("SELECT f.* EXCLUDE (account_canonical_id) FROM t AS f"); + dialects.verified_only_select("SELECT f.* EXCLUDE (col1, col2) FROM t AS f"); +} + #[test] fn test_no_semicolon_required_between_statements() { let sql = r#" diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index e0e3f143b..a061876df 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -156,7 +156,9 @@ fn column_defs(statement: Statement) -> Vec { fn test_select_wildcard_with_exclude() { let select = duckdb().verified_only_select("SELECT * EXCLUDE (col_a) FROM data"); let expected = SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: Some(ExcludeSelectItem::Multiple(vec![Ident::new("col_a")])), + opt_exclude: Some(ExcludeSelectItem::Multiple(vec![ObjectName::from( + Ident::new("col_a"), + )])), ..Default::default() }); assert_eq!(expected, select.projection[0]); @@ -166,7 +168,9 @@ fn test_select_wildcard_with_exclude() { let expected = SelectItem::QualifiedWildcard( SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])), WildcardAdditionalOptions { - opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), + opt_exclude: Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new( + "department_id", + )))), ..Default::default() }, ); @@ -176,8 +180,8 @@ fn test_select_wildcard_with_exclude() { .verified_only_select("SELECT * EXCLUDE (department_id, employee_id) FROM employee_table"); let expected = SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Multiple(vec![ - Ident::new("department_id"), - Ident::new("employee_id"), + ObjectName::from(Ident::new("department_id")), + ObjectName::from(Ident::new("employee_id")), ])), ..Default::default() }); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 43444016f..c51cf3bdf 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1474,7 +1474,9 @@ fn snowflake_and_generic() -> TestedDialects { fn test_select_wildcard_with_exclude() { let select = snowflake_and_generic().verified_only_select("SELECT * EXCLUDE (col_a) FROM data"); let expected = SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: Some(ExcludeSelectItem::Multiple(vec![Ident::new("col_a")])), + opt_exclude: Some(ExcludeSelectItem::Multiple(vec![ObjectName::from( + Ident::new("col_a"), + )])), ..Default::default() }); assert_eq!(expected, select.projection[0]); @@ -1484,7 +1486,9 @@ fn test_select_wildcard_with_exclude() { let expected = SelectItem::QualifiedWildcard( SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])), WildcardAdditionalOptions { - opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), + opt_exclude: Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new( + "department_id", + )))), ..Default::default() }, ); @@ -1494,8 +1498,8 @@ fn test_select_wildcard_with_exclude() { .verified_only_select("SELECT * EXCLUDE (department_id, employee_id) FROM employee_table"); let expected = SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Multiple(vec![ - Ident::new("department_id"), - Ident::new("employee_id"), + ObjectName::from(Ident::new("department_id")), + ObjectName::from(Ident::new("employee_id")), ])), ..Default::default() }); @@ -1580,7 +1584,9 @@ fn test_select_wildcard_with_exclude_and_rename() { let select = snowflake_and_generic() .verified_only_select("SELECT * EXCLUDE col_z RENAME col_a AS col_b FROM data"); let expected = SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("col_z"))), + opt_exclude: Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new( + "col_z", + )))), opt_rename: Some(RenameSelectItem::Single(IdentWithAlias { ident: Ident::new("col_a"), alias: Ident::new("col_b"),