Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ pub struct Select {
pub top_before_distinct: bool,
/// projection expressions
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
pub into: Option<SelectInto>,
/// FROM
Expand Down Expand Up @@ -401,6 +406,10 @@ impl fmt::Display for Select {
indented_list(f, &self.projection)?;
}

if let Some(exclude) = &self.exclude {
write!(f, " {exclude}")?;
}

if let Some(ref into) = self.into {
f.write_str(" ")?;
into.fmt(f)?;
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2220,6 +2220,7 @@ impl Spanned for Select {
distinct: _, // todo
top: _, // todo, mysql specific
projection,
exclude: _,
into,
from,
lateral_views,
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,8 @@ impl Dialect for DuckDbDialect {
fn supports_order_by_all(&self) -> bool {
true
}

fn supports_select_wildcard_exclude(&self) -> bool {
true
}
}
4 changes: 4 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,8 @@ impl Dialect for GenericDialect {
fn supports_filter_during_aggregation(&self) -> bool {
true
}

fn supports_select_wildcard_exclude(&self) -> bool {
true
}
}
20 changes: 20 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,26 @@ pub trait Dialect: Debug + Any {
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
///
/// This method is called to parse the next infix expression.
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/redshift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,12 @@ impl Dialect for RedshiftSqlDialect {
fn supports_string_literal_backslash_escape(&self) -> bool {
true
}

fn supports_select_wildcard_exclude(&self) -> bool {
true
}

fn supports_select_exclude(&self) -> bool {
true
}
}
4 changes: 4 additions & 0 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@ impl Dialect for SnowflakeDialect {
fn supports_select_expr_star(&self) -> bool {
true
}

fn supports_select_wildcard_exclude(&self) -> bool {
true
}
}

fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> {
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
Keyword::FETCH,
Keyword::UNION,
Keyword::EXCEPT,
Keyword::EXCLUDE,
Keyword::INTERSECT,
Keyword::MINUS,
Keyword::CLUSTER,
Expand Down
11 changes: 9 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11740,6 +11740,7 @@ impl<'a> Parser<'a> {
top: None,
top_before_distinct: false,
projection: vec![],
exclude: None,
into: None,
from,
lateral_views: vec![],
Expand Down Expand Up @@ -11782,6 +11783,12 @@ impl<'a> Parser<'a> {
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) {
Some(self.parse_select_into()?)
} else {
Expand Down Expand Up @@ -11915,6 +11922,7 @@ impl<'a> Parser<'a> {
top,
top_before_distinct,
projection,
exclude,
into,
from,
lateral_views,
Expand Down Expand Up @@ -15052,8 +15060,7 @@ impl<'a> Parser<'a> {
} else {
None
};
let opt_exclude = if opt_ilike.is_none()
&& dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect)
let opt_exclude = if opt_ilike.is_none() && self.dialect.supports_select_wildcard_exclude()
{
self.parse_optional_select_item_exclude()?
} else {
Expand Down
1 change: 1 addition & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ fn parse_map_access_expr() {
),
})],
})],
exclude: None,
into: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("foos")])),
Expand Down
115 changes: 115 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ fn parse_update_set_from() {
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))),
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))),
],
exclude: None,
into: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])),
Expand Down Expand Up @@ -5688,6 +5689,7 @@ fn test_parse_named_window() {
},
},
],
exclude: None,
into: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident {
Expand Down Expand Up @@ -6340,6 +6342,7 @@ fn parse_interval_and_or_xor() {
quote_style: None,
span: Span::empty(),
}))],
exclude: None,
into: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident {
Expand Down Expand Up @@ -8608,6 +8611,7 @@ fn lateral_function() {
distinct: None,
top: None,
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
exclude: None,
top_before_distinct: false,
into: None,
from: vec![TableWithJoins {
Expand Down Expand Up @@ -9604,6 +9608,7 @@ fn parse_merge() {
projection: vec![SelectItem::Wildcard(
WildcardAdditionalOptions::default()
)],
exclude: None,
into: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![
Expand Down Expand Up @@ -11522,6 +11527,7 @@ fn parse_unload() {
top: None,
top_before_distinct: false,
projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),],
exclude: None,
into: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("tab")])),
Expand Down Expand Up @@ -11722,6 +11728,7 @@ fn parse_connect_by() {
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))),
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))),
],
exclude: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])),
joins: vec![],
Expand Down Expand Up @@ -11803,6 +11810,7 @@ fn parse_connect_by() {
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))),
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))),
],
exclude: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])),
joins: vec![],
Expand Down Expand Up @@ -12736,6 +12744,7 @@ fn test_extract_seconds_ok() {
format: None,
}),
})],
exclude: None,
into: None,
from: vec![],
lateral_views: vec![],
Expand Down Expand Up @@ -14808,6 +14817,7 @@ fn test_select_from_first() {
distinct: None,
top: None,
projection,
exclude: None,
top_before_distinct: false,
into: None,
from: vec![TableWithJoins {
Expand Down Expand Up @@ -15988,3 +15998,108 @@ fn parse_create_procedure_with_parameter_modes() {
_ => 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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test with

let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude() && !d.supports_select_exclude());

like in snowflake's case to demonstrate the behavior when there is e.g. an exclude that is an ident SELECT *, EXCLUDE c1 FROM test or similar

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())
);
}
2 changes: 2 additions & 0 deletions tests/sqlparser_duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ fn test_select_union_by_name() {
distinct: None,
top: None,
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
exclude: None,
top_before_distinct: false,
into: None,
from: vec![TableWithJoins {
Expand Down Expand Up @@ -299,6 +300,7 @@ fn test_select_union_by_name() {
distinct: None,
top: None,
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
exclude: None,
top_before_distinct: false,
into: None,
from: vec![TableWithJoins {
Expand Down
3 changes: 3 additions & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ fn parse_create_procedure() {
projection: vec![SelectItem::UnnamedExpr(Expr::Value(
(number("1")).with_empty_span()
))],
exclude: None,
into: None,
from: vec![],
lateral_views: vec![],
Expand Down Expand Up @@ -1368,6 +1369,7 @@ fn parse_substring_in_select() {
special: true,
shorthand: false,
})],
exclude: None,
into: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident {
Expand Down Expand Up @@ -1516,6 +1518,7 @@ fn parse_mssql_declare() {
(Value::Number("4".parse().unwrap(), false)).with_empty_span()
)),
})],
exclude: None,
into: None,
from: vec![],
lateral_views: vec![],
Expand Down
Loading
Loading