Add support for Redshift SELECT * EXCLUDE (#1936)
Some checks are pending
license / Release Audit Tool (RAT) (push) Waiting to run
Rust / benchmark-lint (push) Waiting to run
Rust / compile (push) Waiting to run
Rust / docs (push) Waiting to run
Rust / compile-no-std (push) Waiting to run
Rust / test (beta) (push) Waiting to run
Rust / test (nightly) (push) Waiting to run
Rust / test (stable) (push) Waiting to run
Rust / codestyle (push) Waiting to run
Rust / lint (push) Waiting to run

This commit is contained in:
Yoav Cohen 2025-07-11 11:39:29 +02:00 committed by GitHub
parent 15f35e1476
commit ee31b64f9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 192 additions and 3 deletions

View file

@ -321,6 +321,11 @@ pub struct Select {
pub top_before_distinct: bool, pub top_before_distinct: bool,
/// projection expressions /// projection expressions
pub projection: Vec<SelectItem>, pub projection: Vec<SelectItem>,
/// Excluded columns from the projection expression which are not specified
/// directly after a wildcard.
///
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html)
pub exclude: Option<ExcludeSelectItem>,
/// INTO /// INTO
pub into: Option<SelectInto>, pub into: Option<SelectInto>,
/// FROM /// FROM
@ -401,6 +406,10 @@ impl fmt::Display for Select {
indented_list(f, &self.projection)?; indented_list(f, &self.projection)?;
} }
if let Some(exclude) = &self.exclude {
write!(f, " {exclude}")?;
}
if let Some(ref into) = self.into { if let Some(ref into) = self.into {
f.write_str(" ")?; f.write_str(" ")?;
into.fmt(f)?; into.fmt(f)?;

View file

@ -2220,6 +2220,7 @@ impl Spanned for Select {
distinct: _, // todo distinct: _, // todo
top: _, // todo, mysql specific top: _, // todo, mysql specific
projection, projection,
exclude: _,
into, into,
from, from,
lateral_views, lateral_views,

View file

@ -94,4 +94,8 @@ impl Dialect for DuckDbDialect {
fn supports_order_by_all(&self) -> bool { fn supports_order_by_all(&self) -> bool {
true true
} }
fn supports_select_wildcard_exclude(&self) -> bool {
true
}
} }

View file

@ -179,4 +179,8 @@ impl Dialect for GenericDialect {
fn supports_filter_during_aggregation(&self) -> bool { fn supports_filter_during_aggregation(&self) -> bool {
true true
} }
fn supports_select_wildcard_exclude(&self) -> bool {
true
}
} }

View file

@ -570,6 +570,26 @@ pub trait Dialect: Debug + Any {
false false
} }
/// Returns true if the dialect supports an exclude option
/// following a wildcard in the projection section. For example:
/// `SELECT * EXCLUDE col1 FROM tbl`.
///
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html)
/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/select)
fn supports_select_wildcard_exclude(&self) -> bool {
false
}
/// Returns true if the dialect supports an exclude option
/// as the last item in the projection section, not necessarily
/// after a wildcard. For example:
/// `SELECT *, c1, c2 EXCLUDE c3 FROM tbl`
///
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html)
fn supports_select_exclude(&self) -> bool {
false
}
/// Dialect-specific infix parser override /// Dialect-specific infix parser override
/// ///
/// This method is called to parse the next infix expression. /// This method is called to parse the next infix expression.

View file

@ -131,4 +131,12 @@ impl Dialect for RedshiftSqlDialect {
fn supports_string_literal_backslash_escape(&self) -> bool { fn supports_string_literal_backslash_escape(&self) -> bool {
true true
} }
fn supports_select_wildcard_exclude(&self) -> bool {
true
}
fn supports_select_exclude(&self) -> bool {
true
}
} }

View file

@ -466,6 +466,10 @@ impl Dialect for SnowflakeDialect {
fn supports_select_expr_star(&self) -> bool { fn supports_select_expr_star(&self) -> bool {
true true
} }
fn supports_select_wildcard_exclude(&self) -> bool {
true
}
} }
fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> { fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> {

View file

@ -1119,6 +1119,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
Keyword::FETCH, Keyword::FETCH,
Keyword::UNION, Keyword::UNION,
Keyword::EXCEPT, Keyword::EXCEPT,
Keyword::EXCLUDE,
Keyword::INTERSECT, Keyword::INTERSECT,
Keyword::MINUS, Keyword::MINUS,
Keyword::CLUSTER, Keyword::CLUSTER,

View file

@ -11740,6 +11740,7 @@ impl<'a> Parser<'a> {
top: None, top: None,
top_before_distinct: false, top_before_distinct: false,
projection: vec![], projection: vec![],
exclude: None,
into: None, into: None,
from, from,
lateral_views: vec![], lateral_views: vec![],
@ -11782,6 +11783,12 @@ impl<'a> Parser<'a> {
self.parse_projection()? self.parse_projection()?
}; };
let exclude = if self.dialect.supports_select_exclude() {
self.parse_optional_select_item_exclude()?
} else {
None
};
let into = if self.parse_keyword(Keyword::INTO) { let into = if self.parse_keyword(Keyword::INTO) {
Some(self.parse_select_into()?) Some(self.parse_select_into()?)
} else { } else {
@ -11915,6 +11922,7 @@ impl<'a> Parser<'a> {
top, top,
top_before_distinct, top_before_distinct,
projection, projection,
exclude,
into, into,
from, from,
lateral_views, lateral_views,
@ -15052,8 +15060,7 @@ impl<'a> Parser<'a> {
} else { } else {
None None
}; };
let opt_exclude = if opt_ilike.is_none() let opt_exclude = if opt_ilike.is_none() && self.dialect.supports_select_wildcard_exclude()
&& dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect)
{ {
self.parse_optional_select_item_exclude()? self.parse_optional_select_item_exclude()?
} else { } else {

View file

@ -60,6 +60,7 @@ fn parse_map_access_expr() {
), ),
})], })],
})], })],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("foos")])), relation: table_from_name(ObjectName::from(vec![Ident::new("foos")])),

View file

@ -459,6 +459,7 @@ fn parse_update_set_from() {
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))),
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))),
], ],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])),
@ -5695,6 +5696,7 @@ fn test_parse_named_window() {
}, },
}, },
], ],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident { relation: table_from_name(ObjectName::from(vec![Ident {
@ -6351,6 +6353,7 @@ fn parse_interval_and_or_xor() {
quote_style: None, quote_style: None,
span: Span::empty(), span: Span::empty(),
}))], }))],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident { relation: table_from_name(ObjectName::from(vec![Ident {
@ -8620,6 +8623,7 @@ fn lateral_function() {
distinct: None, distinct: None,
top: None, top: None,
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
exclude: None,
top_before_distinct: false, top_before_distinct: false,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
@ -9616,6 +9620,7 @@ fn parse_merge() {
projection: vec![SelectItem::Wildcard( projection: vec![SelectItem::Wildcard(
WildcardAdditionalOptions::default() WildcardAdditionalOptions::default()
)], )],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![ relation: table_from_name(ObjectName::from(vec![
@ -11534,6 +11539,7 @@ fn parse_unload() {
top: None, top: None,
top_before_distinct: false, top_before_distinct: false,
projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),], projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("tab")])), relation: table_from_name(ObjectName::from(vec![Ident::new("tab")])),
@ -11734,6 +11740,7 @@ fn parse_connect_by() {
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))),
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))),
], ],
exclude: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])),
joins: vec![], joins: vec![],
@ -11815,6 +11822,7 @@ fn parse_connect_by() {
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))),
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))),
], ],
exclude: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])),
joins: vec![], joins: vec![],
@ -12748,6 +12756,7 @@ fn test_extract_seconds_ok() {
format: None, format: None,
}), }),
})], })],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
@ -14820,6 +14829,7 @@ fn test_select_from_first() {
distinct: None, distinct: None,
top: None, top: None,
projection, projection,
exclude: None,
top_before_distinct: false, top_before_distinct: false,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
@ -16000,3 +16010,108 @@ fn parse_create_procedure_with_parameter_modes() {
_ => unreachable!(), _ => unreachable!(),
} }
} }
#[test]
fn test_select_exclude() {
let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude());
match &dialects
.verified_only_select("SELECT * EXCLUDE c1 FROM test")
.projection[0]
{
SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => {
assert_eq!(
*opt_exclude,
Some(ExcludeSelectItem::Single(Ident::new("c1")))
);
}
_ => unreachable!(),
}
match &dialects
.verified_only_select("SELECT * EXCLUDE (c1, c2) FROM test")
.projection[0]
{
SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => {
assert_eq!(
*opt_exclude,
Some(ExcludeSelectItem::Multiple(vec![
Ident::new("c1"),
Ident::new("c2")
]))
);
}
_ => unreachable!(),
}
let select = dialects.verified_only_select("SELECT * EXCLUDE c1, c2 FROM test");
match &select.projection[0] {
SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => {
assert_eq!(
*opt_exclude,
Some(ExcludeSelectItem::Single(Ident::new("c1")))
);
}
_ => unreachable!(),
}
match &select.projection[1] {
SelectItem::UnnamedExpr(Expr::Identifier(ident)) => {
assert_eq!(*ident, Ident::new("c2"));
}
_ => unreachable!(),
}
let dialects = all_dialects_where(|d| d.supports_select_exclude());
let select = dialects.verified_only_select("SELECT *, c1 EXCLUDE c1 FROM test");
match &select.projection[0] {
SelectItem::Wildcard(additional_options) => {
assert_eq!(*additional_options, WildcardAdditionalOptions::default());
}
_ => unreachable!(),
}
assert_eq!(
select.exclude,
Some(ExcludeSelectItem::Single(Ident::new("c1")))
);
let dialects = all_dialects_where(|d| {
d.supports_select_wildcard_exclude() && !d.supports_select_exclude()
});
let select = dialects.verified_only_select("SELECT * EXCLUDE c1 FROM test");
match &select.projection[0] {
SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => {
assert_eq!(
*opt_exclude,
Some(ExcludeSelectItem::Single(Ident::new("c1")))
);
}
_ => unreachable!(),
}
// Dialects that only support the wildcard form and do not accept EXCLUDE as an implicity alias
// will fail when encountered with the `c2` ident
let dialects = all_dialects_where(|d| {
d.supports_select_wildcard_exclude()
&& !d.supports_select_exclude()
&& d.is_column_alias(&Keyword::EXCLUDE, &mut Parser::new(d))
});
assert_eq!(
dialects
.parse_sql_statements("SELECT *, c1 EXCLUDE c2 FROM test")
.err()
.unwrap(),
ParserError::ParserError("Expected: end of statement, found: c2".to_string())
);
// Dialects that only support the wildcard form and accept EXCLUDE as an implicity alias
// will fail when encountered with the `EXCLUDE` keyword
let dialects = all_dialects_where(|d| {
d.supports_select_wildcard_exclude()
&& !d.supports_select_exclude()
&& !d.is_column_alias(&Keyword::EXCLUDE, &mut Parser::new(d))
});
assert_eq!(
dialects
.parse_sql_statements("SELECT *, c1 EXCLUDE c2 FROM test")
.err()
.unwrap(),
ParserError::ParserError("Expected: end of statement, found: EXCLUDE".to_string())
);
}

View file

@ -269,6 +269,7 @@ fn test_select_union_by_name() {
distinct: None, distinct: None,
top: None, top: None,
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
exclude: None,
top_before_distinct: false, top_before_distinct: false,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
@ -299,6 +300,7 @@ fn test_select_union_by_name() {
distinct: None, distinct: None,
top: None, top: None,
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
exclude: None,
top_before_distinct: false, top_before_distinct: false,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {

View file

@ -126,6 +126,7 @@ fn parse_create_procedure() {
projection: vec![SelectItem::UnnamedExpr(Expr::Value( projection: vec![SelectItem::UnnamedExpr(Expr::Value(
(number("1")).with_empty_span() (number("1")).with_empty_span()
))], ))],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
@ -1368,6 +1369,7 @@ fn parse_substring_in_select() {
special: true, special: true,
shorthand: false, shorthand: false,
})], })],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident { relation: table_from_name(ObjectName::from(vec![Ident {
@ -1516,6 +1518,7 @@ fn parse_mssql_declare() {
(Value::Number("4".parse().unwrap(), false)).with_empty_span() (Value::Number("4".parse().unwrap(), false)).with_empty_span()
)), )),
})], })],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],

View file

@ -1403,6 +1403,7 @@ fn parse_escaped_quote_identifiers_with_escape() {
quote_style: Some('`'), quote_style: Some('`'),
span: Span::empty(), span: Span::empty(),
}))], }))],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
@ -1456,6 +1457,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() {
quote_style: Some('`'), quote_style: Some('`'),
span: Span::empty(), span: Span::empty(),
}))], }))],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
@ -1503,6 +1505,7 @@ fn parse_escaped_backticks_with_escape() {
quote_style: Some('`'), quote_style: Some('`'),
span: Span::empty(), span: Span::empty(),
}))], }))],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
@ -1554,6 +1557,7 @@ fn parse_escaped_backticks_with_no_escape() {
quote_style: Some('`'), quote_style: Some('`'),
span: Span::empty(), span: Span::empty(),
}))], }))],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
@ -2225,6 +2229,7 @@ fn parse_select_with_numeric_prefix_column_name() {
projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new( projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new(
"123col_$@123abc" "123col_$@123abc"
)))], )))],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::with_quote( relation: table_from_name(ObjectName::from(vec![Ident::with_quote(
@ -2392,7 +2397,6 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() {
q.body, q.body,
Box::new(SetExpr::Select(Box::new(Select { Box::new(SetExpr::Select(Box::new(Select {
select_token: AttachedToken::empty(), select_token: AttachedToken::empty(),
distinct: None, distinct: None,
top: None, top: None,
top_before_distinct: false, top_before_distinct: false,
@ -2400,6 +2404,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() {
SelectItem::UnnamedExpr(Expr::value(number("123e4"))), SelectItem::UnnamedExpr(Expr::value(number("123e4"))),
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("123col_$@123abc"))) SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("123col_$@123abc")))
], ],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::with_quote( relation: table_from_name(ObjectName::from(vec![Ident::with_quote(
@ -3043,6 +3048,7 @@ fn parse_substring_in_select() {
special: true, special: true,
shorthand: false, shorthand: false,
})], })],
exclude: None,
into: None, into: None,
from: vec![TableWithJoins { from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident { relation: table_from_name(ObjectName::from(vec![Ident {
@ -3357,6 +3363,7 @@ fn parse_hex_string_introducer() {
) )
.into(), .into(),
})], })],
exclude: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
prewhere: None, prewhere: None,

View file

@ -1305,6 +1305,7 @@ fn parse_copy_to() {
}, },
} }
], ],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
@ -2948,6 +2949,7 @@ fn parse_array_subquery_expr() {
projection: vec![SelectItem::UnnamedExpr(Expr::Value( projection: vec![SelectItem::UnnamedExpr(Expr::Value(
(number("1")).with_empty_span() (number("1")).with_empty_span()
))], ))],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],
@ -2973,6 +2975,7 @@ fn parse_array_subquery_expr() {
projection: vec![SelectItem::UnnamedExpr(Expr::Value( projection: vec![SelectItem::UnnamedExpr(Expr::Value(
(number("2")).with_empty_span() (number("2")).with_empty_span()
))], ))],
exclude: None,
into: None, into: None,
from: vec![], from: vec![],
lateral_views: vec![], lateral_views: vec![],