diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3fe548c81..7f3d15438 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -31,9 +31,10 @@ pub use self::ddl::{ }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, ExcludeSelectItem, Fetch, Join, JoinConstraint, JoinOperator, LateralView, LockType, - Offset, OffsetRows, OrderByExpr, Query, Select, SelectInto, SelectItem, SetExpr, SetOperator, - SetQuantifier, Table, TableAlias, TableFactor, TableWithJoins, Top, Values, With, + Cte, ExceptSelectItem, ExcludeSelectItem, Fetch, Join, JoinConstraint, JoinOperator, + LateralView, LockType, Offset, OffsetRows, OrderByExpr, Query, Select, SelectInto, SelectItem, + SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, TableWithJoins, Top, + Values, WildcardAdditionalOptions, With, }; pub use self::value::{escape_quoted_string, DateTimeField, TrimWhereField, Value}; diff --git a/src/ast/query.rs b/src/ast/query.rs index 6cccb6e44..f813f44dd 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -347,9 +347,31 @@ pub enum SelectItem { /// An expression, followed by `[ AS ] alias` ExprWithAlias { expr: Expr, alias: Ident }, /// `alias.*` or even `schema.table.*` - QualifiedWildcard(ObjectName, Option), + QualifiedWildcard(ObjectName, WildcardAdditionalOptions), /// An unqualified `*` - Wildcard(Option), + Wildcard(WildcardAdditionalOptions), +} + +/// Additional options for wildcards, e.g. Snowflake `EXCLUDE` and Bigquery `EXCEPT`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct WildcardAdditionalOptions { + /// `[EXCLUDE...]`. + pub opt_exclude: Option, + /// `[EXCEPT...]`. + pub opt_except: Option, +} + +impl fmt::Display for WildcardAdditionalOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(exclude) = &self.opt_exclude { + write!(f, " {exclude}")?; + } + if let Some(except) = &self.opt_except { + write!(f, " {except}")?; + } + Ok(()) + } } /// Snowflake `EXCLUDE` information. @@ -392,23 +414,51 @@ impl fmt::Display for ExcludeSelectItem { } } +/// Bigquery `EXCEPT` information, with at least one column. +/// +/// # Syntax +/// ```plaintext +/// EXCEPT ( [, ...]) +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ExceptSelectItem { + /// First guaranteed column. + pub fist_elemnt: Ident, + /// Additional columns. This list can be empty. + pub additional_elements: Vec, +} + +impl fmt::Display for ExceptSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "EXCEPT ")?; + if self.additional_elements.is_empty() { + write!(f, "({})", self.fist_elemnt)?; + } else { + write!( + f, + "({}, {})", + self.fist_elemnt, + display_comma_separated(&self.additional_elements) + )?; + } + Ok(()) + } +} + impl fmt::Display for SelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { SelectItem::UnnamedExpr(expr) => write!(f, "{}", expr), SelectItem::ExprWithAlias { expr, alias } => write!(f, "{} AS {}", expr, alias), - SelectItem::QualifiedWildcard(prefix, opt_exclude) => { + SelectItem::QualifiedWildcard(prefix, additional_options) => { write!(f, "{}.*", prefix)?; - if let Some(exclude) = opt_exclude { - write!(f, " {exclude}")?; - } + write!(f, "{additional_options}")?; Ok(()) } - SelectItem::Wildcard(opt_exclude) => { + SelectItem::Wildcard(additional_options) => { write!(f, "*")?; - if let Some(exclude) = opt_exclude { - write!(f, " {exclude}")?; - } + write!(f, "{additional_options}")?; Ok(()) } } diff --git a/src/parser.rs b/src/parser.rs index 29577f053..d1d26293d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5643,25 +5643,37 @@ impl<'a> Parser<'a> { None => SelectItem::UnnamedExpr(expr), }) } - WildcardExpr::QualifiedWildcard(prefix) => { - let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { - self.parse_optional_select_item_exclude()? - } else { - None - }; + WildcardExpr::QualifiedWildcard(prefix) => Ok(SelectItem::QualifiedWildcard( + prefix, + self.parse_wildcard_additional_options()?, + )), + WildcardExpr::Wildcard => Ok(SelectItem::Wildcard( + self.parse_wildcard_additional_options()?, + )), + } + } - Ok(SelectItem::QualifiedWildcard(prefix, opt_exclude)) - } - WildcardExpr::Wildcard => { - let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { - self.parse_optional_select_item_exclude()? - } else { - None - }; + /// Parse an [`WildcardAdditionalOptions`](WildcardAdditionalOptions) information for wildcard select items. + /// + /// If it is not possible to parse it, will return an option. + pub fn parse_wildcard_additional_options( + &mut self, + ) -> Result { + let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_exclude()? + } else { + None + }; + let opt_except = if dialect_of!(self is GenericDialect | BigQueryDialect) { + self.parse_optional_select_item_except()? + } else { + None + }; - Ok(SelectItem::Wildcard(opt_exclude)) - } - } + Ok(WildcardAdditionalOptions { + opt_exclude, + opt_except, + }) } /// Parse an [`Exclude`](ExcludeSelectItem) information for wildcard select items. @@ -5686,6 +5698,33 @@ impl<'a> Parser<'a> { Ok(opt_exclude) } + /// Parse an [`Except`](ExceptSelectItem) information for wildcard select items. + /// + /// If it is not possible to parse it, will return an option. + pub fn parse_optional_select_item_except( + &mut self, + ) -> Result, ParserError> { + let opt_except = if self.parse_keyword(Keyword::EXCEPT) { + let idents = self.parse_parenthesized_column_list(Mandatory)?; + match &idents[..] { + [] => { + return self.expected( + "at least one column should be parsed by the expect clause", + self.peek_token(), + )?; + } + [first, idents @ ..] => Some(ExceptSelectItem { + fist_elemnt: first.clone(), + additional_elements: idents.to_vec(), + }), + } + } else { + None + }; + + Ok(opt_except) + } + /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parse_expr()?; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 8ada172cf..90ca27a69 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -16,7 +16,7 @@ mod test_utils; use test_utils::*; use sqlparser::ast::*; -use sqlparser::dialect::BigQueryDialect; +use sqlparser::dialect::{BigQueryDialect, GenericDialect}; #[test] fn parse_literal_string() { @@ -235,8 +235,85 @@ fn parse_array_agg_func() { } } +#[test] +fn test_select_wildcard_with_except() { + match bigquery_and_generic().verified_stmt("SELECT * EXCEPT (col_a) FROM data") { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_except: Some(except), + .. + }) => { + assert_eq!( + *except, + ExceptSelectItem { + fist_elemnt: Ident::new("col_a"), + additional_elements: vec![] + } + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; + + match bigquery_and_generic() + .verified_stmt("SELECT * EXCEPT (department_id, employee_id) FROM employee_table") + { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_except: Some(except), + .. + }) => { + assert_eq!( + *except, + ExceptSelectItem { + fist_elemnt: Ident::new("department_id"), + additional_elements: vec![Ident::new("employee_id")] + } + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; + + match bigquery_and_generic().verified_stmt("SELECT * EXCEPT (col1, col2) FROM _table") { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_except: Some(except), + .. + }) => { + assert_eq!( + *except, + ExceptSelectItem { + fist_elemnt: Ident::new("col1"), + additional_elements: vec![Ident::new("col2")] + } + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; +} + fn bigquery() -> TestedDialects { TestedDialects { dialects: vec![Box::new(BigQueryDialect {})], } } + +fn bigquery_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(BigQueryDialect {}), Box::new(GenericDialect {})], + } +} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 24d101919..addc68e47 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -581,12 +581,18 @@ fn parse_select_into() { fn parse_select_wildcard() { let sql = "SELECT * FROM foo"; let select = verified_only_select(sql); - assert_eq!(&SelectItem::Wildcard(None), only(&select.projection)); + assert_eq!( + &SelectItem::Wildcard(WildcardAdditionalOptions::default()), + only(&select.projection) + ); let sql = "SELECT foo.* FROM foo"; let select = verified_only_select(sql); assert_eq!( - &SelectItem::QualifiedWildcard(ObjectName(vec![Ident::new("foo")]), None), + &SelectItem::QualifiedWildcard( + ObjectName(vec![Ident::new("foo")]), + WildcardAdditionalOptions::default() + ), only(&select.projection) ); @@ -595,7 +601,7 @@ fn parse_select_wildcard() { assert_eq!( &SelectItem::QualifiedWildcard( ObjectName(vec![Ident::new("myschema"), Ident::new("mytable"),]), - None + WildcardAdditionalOptions::default(), ), only(&select.projection) ); @@ -5588,7 +5594,9 @@ fn parse_merge() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: false, top: None, - projection: vec![SelectItem::Wildcard(None)], + projection: vec![SelectItem::Wildcard( + WildcardAdditionalOptions::default() + )], into: None, from: vec![TableWithJoins { relation: TableFactor::Table { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d1cd60ff3..fac98b8ef 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1277,7 +1277,12 @@ fn parse_pg_returning() { pg_and_generic().verified_stmt("DELETE FROM tasks WHERE status = 'DONE' RETURNING *"); match stmt { Statement::Delete { returning, .. } => { - assert_eq!(Some(vec![SelectItem::Wildcard(None),]), returning); + assert_eq!( + Some(vec![SelectItem::Wildcard( + WildcardAdditionalOptions::default() + ),]), + returning + ); } _ => unreachable!(), }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 127528b82..1ec57939c 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -370,7 +370,10 @@ fn test_select_wildcard_with_exclude() { match snowflake_and_generic().verified_stmt("SELECT * EXCLUDE (col_a) FROM data") { Statement::Query(query) => match *query.body { SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(Some(exclude)) => { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(exclude), + .. + }) => { assert_eq!( *exclude, ExcludeSelectItem::Multiple(vec![Ident::new("col_a")]) @@ -388,7 +391,13 @@ fn test_select_wildcard_with_exclude() { { Statement::Query(query) => match *query.body { SetExpr::Select(select) => match &select.projection[0] { - SelectItem::QualifiedWildcard(_, Some(exclude)) => { + SelectItem::QualifiedWildcard( + _, + WildcardAdditionalOptions { + opt_exclude: Some(exclude), + .. + }, + ) => { assert_eq!( *exclude, ExcludeSelectItem::Single(Ident::new("department_id")) @@ -406,7 +415,10 @@ fn test_select_wildcard_with_exclude() { { Statement::Query(query) => match *query.body { SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(Some(exclude)) => { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(exclude), + .. + }) => { assert_eq!( *exclude, ExcludeSelectItem::Multiple(vec![