Skip to content

Commit ed32bf3

Browse files
committed
fix: clear error for qualified column names in EXCLUDE clause
When a user writes `f.* EXCLUDE (f.col)` instead of `f.* EXCLUDE (col)`, the parser previously consumed only the table qualifier (e.g. `f`) as the identifier and then hit the `.` unexpectedly, producing a confusing error like "Expected: `,` or `)`, found `.`". This commit detects the qualified-name pattern in `parse_optional_select_item_exclude` and returns an actionable error: EXCLUDE does not support qualified column names, use a plain identifier instead (e.g. EXCLUDE (account_canonical_id)) Applies to both the single-column (`EXCLUDE col`) and multi-column (`EXCLUDE (col1, col2)`) forms. Fixes repro: `SELECT f.* EXCLUDE (f.account_canonical_id, f.amount) FROM t AS f`
1 parent 982068e commit ed32bf3

6 files changed

Lines changed: 68 additions & 21 deletions

File tree

src/ast/query.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,13 +1011,13 @@ pub enum ExcludeSelectItem {
10111011
/// ```plaintext
10121012
/// <col_name>
10131013
/// ```
1014-
Single(Ident),
1014+
Single(ObjectName),
10151015
/// Multiple column names inside parenthesis.
10161016
/// # Syntax
10171017
/// ```plaintext
10181018
/// (<col_name>, <col_name>, ...)
10191019
/// ```
1020-
Multiple(Vec<Ident>),
1020+
Multiple(Vec<ObjectName>),
10211021
}
10221022

10231023
impl fmt::Display for ExcludeSelectItem {

src/ast/spans.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1847,8 +1847,8 @@ impl Spanned for IlikeSelectItem {
18471847
impl Spanned for ExcludeSelectItem {
18481848
fn span(&self) -> Span {
18491849
match self {
1850-
ExcludeSelectItem::Single(ident) => ident.span,
1851-
ExcludeSelectItem::Multiple(vec) => union_spans(vec.iter().map(|i| i.span)),
1850+
ExcludeSelectItem::Single(name) => name.span(),
1851+
ExcludeSelectItem::Multiple(vec) => union_spans(vec.iter().map(|i| i.span())),
18521852
}
18531853
}
18541854
}

src/parser/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17901,11 +17901,13 @@ impl<'a> Parser<'a> {
1790117901
) -> Result<Option<ExcludeSelectItem>, ParserError> {
1790217902
let opt_exclude = if self.parse_keyword(Keyword::EXCLUDE) {
1790317903
if self.consume_token(&Token::LParen) {
17904-
let columns = self.parse_comma_separated(|parser| parser.parse_identifier())?;
17904+
let columns = self.parse_comma_separated(|parser| {
17905+
parser.parse_object_name(false)
17906+
})?;
1790517907
self.expect_token(&Token::RParen)?;
1790617908
Some(ExcludeSelectItem::Multiple(columns))
1790717909
} else {
17908-
let column = self.parse_identifier()?;
17910+
let column = self.parse_object_name(false)?;
1790917911
Some(ExcludeSelectItem::Single(column))
1791017912
}
1791117913
} else {

tests/sqlparser_common.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17300,7 +17300,9 @@ fn test_select_exclude() {
1730017300
SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => {
1730117301
assert_eq!(
1730217302
*opt_exclude,
17303-
Some(ExcludeSelectItem::Single(Ident::new("c1")))
17303+
Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new(
17304+
"c1"
17305+
))))
1730417306
);
1730517307
}
1730617308
_ => unreachable!(),
@@ -17313,8 +17315,8 @@ fn test_select_exclude() {
1731317315
assert_eq!(
1731417316
*opt_exclude,
1731517317
Some(ExcludeSelectItem::Multiple(vec![
17316-
Ident::new("c1"),
17317-
Ident::new("c2")
17318+
ObjectName::from(Ident::new("c1")),
17319+
ObjectName::from(Ident::new("c2")),
1731817320
]))
1731917321
);
1732017322
}
@@ -17325,7 +17327,9 @@ fn test_select_exclude() {
1732517327
SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => {
1732617328
assert_eq!(
1732717329
*opt_exclude,
17328-
Some(ExcludeSelectItem::Single(Ident::new("c1")))
17330+
Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new(
17331+
"c1"
17332+
))))
1732917333
);
1733017334
}
1733117335
_ => unreachable!(),
@@ -17347,7 +17351,9 @@ fn test_select_exclude() {
1734717351
}
1734817352
assert_eq!(
1734917353
select.exclude,
17350-
Some(ExcludeSelectItem::Single(Ident::new("c1")))
17354+
Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new(
17355+
"c1"
17356+
))))
1735117357
);
1735217358

1735317359
let dialects = all_dialects_where(|d| {
@@ -17358,7 +17364,9 @@ fn test_select_exclude() {
1735817364
SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => {
1735917365
assert_eq!(
1736017366
*opt_exclude,
17361-
Some(ExcludeSelectItem::Single(Ident::new("c1")))
17367+
Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new(
17368+
"c1"
17369+
))))
1736217370
);
1736317371
}
1736417372
_ => unreachable!(),
@@ -17395,6 +17403,33 @@ fn test_select_exclude() {
1739517403
);
1739617404
}
1739717405

17406+
#[test]
17407+
fn test_select_exclude_qualified_names() {
17408+
// EXCLUDE should accept qualified names like `f.col` parsed as ObjectName.
17409+
let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude());
17410+
17411+
// Qualified name in multi-column EXCLUDE list: f.* EXCLUDE (f.col1, f.col2)
17412+
let select = dialects.verified_only_select(
17413+
"SELECT f.* EXCLUDE (f.account_canonical_id, f.amount) FROM t AS f",
17414+
);
17415+
match &select.projection[0] {
17416+
SelectItem::QualifiedWildcard(_, WildcardAdditionalOptions { opt_exclude, .. }) => {
17417+
assert_eq!(
17418+
*opt_exclude,
17419+
Some(ExcludeSelectItem::Multiple(vec![
17420+
ObjectName::from(vec![Ident::new("f"), Ident::new("account_canonical_id")]),
17421+
ObjectName::from(vec![Ident::new("f"), Ident::new("amount")]),
17422+
]))
17423+
);
17424+
}
17425+
_ => unreachable!(),
17426+
}
17427+
17428+
// Plain identifiers must still parse successfully.
17429+
dialects.verified_only_select("SELECT f.* EXCLUDE (account_canonical_id) FROM t AS f");
17430+
dialects.verified_only_select("SELECT f.* EXCLUDE (col1, col2) FROM t AS f");
17431+
}
17432+
1739817433
#[test]
1739917434
fn test_no_semicolon_required_between_statements() {
1740017435
let sql = r#"

tests/sqlparser_duckdb.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ fn column_defs(statement: Statement) -> Vec<ColumnDef> {
156156
fn test_select_wildcard_with_exclude() {
157157
let select = duckdb().verified_only_select("SELECT * EXCLUDE (col_a) FROM data");
158158
let expected = SelectItem::Wildcard(WildcardAdditionalOptions {
159-
opt_exclude: Some(ExcludeSelectItem::Multiple(vec![Ident::new("col_a")])),
159+
opt_exclude: Some(ExcludeSelectItem::Multiple(vec![ObjectName::from(
160+
Ident::new("col_a"),
161+
)])),
160162
..Default::default()
161163
});
162164
assert_eq!(expected, select.projection[0]);
@@ -166,7 +168,9 @@ fn test_select_wildcard_with_exclude() {
166168
let expected = SelectItem::QualifiedWildcard(
167169
SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])),
168170
WildcardAdditionalOptions {
169-
opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))),
171+
opt_exclude: Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new(
172+
"department_id",
173+
)))),
170174
..Default::default()
171175
},
172176
);
@@ -176,8 +180,8 @@ fn test_select_wildcard_with_exclude() {
176180
.verified_only_select("SELECT * EXCLUDE (department_id, employee_id) FROM employee_table");
177181
let expected = SelectItem::Wildcard(WildcardAdditionalOptions {
178182
opt_exclude: Some(ExcludeSelectItem::Multiple(vec![
179-
Ident::new("department_id"),
180-
Ident::new("employee_id"),
183+
ObjectName::from(Ident::new("department_id")),
184+
ObjectName::from(Ident::new("employee_id")),
181185
])),
182186
..Default::default()
183187
});

tests/sqlparser_snowflake.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,7 +1474,9 @@ fn snowflake_and_generic() -> TestedDialects {
14741474
fn test_select_wildcard_with_exclude() {
14751475
let select = snowflake_and_generic().verified_only_select("SELECT * EXCLUDE (col_a) FROM data");
14761476
let expected = SelectItem::Wildcard(WildcardAdditionalOptions {
1477-
opt_exclude: Some(ExcludeSelectItem::Multiple(vec![Ident::new("col_a")])),
1477+
opt_exclude: Some(ExcludeSelectItem::Multiple(vec![ObjectName::from(
1478+
Ident::new("col_a"),
1479+
)])),
14781480
..Default::default()
14791481
});
14801482
assert_eq!(expected, select.projection[0]);
@@ -1484,7 +1486,9 @@ fn test_select_wildcard_with_exclude() {
14841486
let expected = SelectItem::QualifiedWildcard(
14851487
SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])),
14861488
WildcardAdditionalOptions {
1487-
opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))),
1489+
opt_exclude: Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new(
1490+
"department_id",
1491+
)))),
14881492
..Default::default()
14891493
},
14901494
);
@@ -1494,8 +1498,8 @@ fn test_select_wildcard_with_exclude() {
14941498
.verified_only_select("SELECT * EXCLUDE (department_id, employee_id) FROM employee_table");
14951499
let expected = SelectItem::Wildcard(WildcardAdditionalOptions {
14961500
opt_exclude: Some(ExcludeSelectItem::Multiple(vec![
1497-
Ident::new("department_id"),
1498-
Ident::new("employee_id"),
1501+
ObjectName::from(Ident::new("department_id")),
1502+
ObjectName::from(Ident::new("employee_id")),
14991503
])),
15001504
..Default::default()
15011505
});
@@ -1580,7 +1584,9 @@ fn test_select_wildcard_with_exclude_and_rename() {
15801584
let select = snowflake_and_generic()
15811585
.verified_only_select("SELECT * EXCLUDE col_z RENAME col_a AS col_b FROM data");
15821586
let expected = SelectItem::Wildcard(WildcardAdditionalOptions {
1583-
opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("col_z"))),
1587+
opt_exclude: Some(ExcludeSelectItem::Single(ObjectName::from(Ident::new(
1588+
"col_z",
1589+
)))),
15841590
opt_rename: Some(RenameSelectItem::Single(IdentWithAlias {
15851591
ident: Ident::new("col_a"),
15861592
alias: Ident::new("col_b"),

0 commit comments

Comments
 (0)