From 8afcad8fcb915d0366d4cdda9aa9958365f968ae Mon Sep 17 00:00:00 2001 From: Yoabot Date: Thu, 26 Feb 2026 11:37:30 +0100 Subject: [PATCH 01/10] Redshift: support wildcard select items with alias (#2230) --- src/ast/query.rs | 7 +++++++ src/ast/spans.rs | 4 +++- src/dialect/mod.rs | 12 ++++++++++++ src/dialect/redshift.rs | 4 ++++ src/parser/mod.rs | 11 +++++++++++ tests/sqlparser_common.rs | 20 ++++++++++++++++++++ 6 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index b4d3fdb2b..159f02a6c 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -934,6 +934,9 @@ pub struct WildcardAdditionalOptions { pub opt_replace: Option, /// `[RENAME ...]`. pub opt_rename: Option, + /// `[AS ]`. + /// Redshift syntax: + pub opt_alias: Option, } impl Default for WildcardAdditionalOptions { @@ -945,6 +948,7 @@ impl Default for WildcardAdditionalOptions { opt_except: None, opt_replace: None, opt_rename: None, + opt_alias: None, } } } @@ -966,6 +970,9 @@ impl fmt::Display for WildcardAdditionalOptions { if let Some(rename) = &self.opt_rename { write!(f, " {rename}")?; } + if let Some(alias) = &self.opt_alias { + write!(f, " AS {alias}")?; + } Ok(()) } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index b29a134b4..1e2162080 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1824,6 +1824,7 @@ impl Spanned for WildcardAdditionalOptions { opt_except, opt_replace, opt_rename, + opt_alias, } = self; union_spans( @@ -1832,7 +1833,8 @@ impl Spanned for WildcardAdditionalOptions { .chain(opt_exclude.as_ref().map(|i| i.span())) .chain(opt_rename.as_ref().map(|i| i.span())) .chain(opt_replace.as_ref().map(|i| i.span())) - .chain(opt_except.as_ref().map(|i| i.span())), + .chain(opt_except.as_ref().map(|i| i.span())) + .chain(opt_alias.as_ref().map(|i| i.span)), ) } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index bcca455ec..698c12ec9 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1517,6 +1517,18 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect supports aliasing a wildcard select item. + /// + /// Example: + /// ```sql + /// SELECT t.* AS alias FROM t + /// ``` + /// + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_SELECT_list.html) + fn supports_select_wildcard_with_alias(&self) -> bool { + false + } + /// Returns true if this dialect supports the `OPTIMIZE TABLE` statement. /// /// Example: diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 21958e382..5969ee55e 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -141,6 +141,10 @@ impl Dialect for RedshiftSqlDialect { true } + fn supports_select_wildcard_with_alias(&self) -> bool { + true + } + fn supports_select_exclude(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bb11d79c2..667a15352 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17864,6 +17864,16 @@ impl<'a> Parser<'a> { None }; + let opt_alias = if self.dialect.supports_select_wildcard_with_alias() { + if self.parse_keyword(Keyword::AS) { + Some(self.parse_identifier()?) + } else { + None + } + } else { + None + }; + Ok(WildcardAdditionalOptions { wildcard_token: wildcard_token.into(), opt_ilike, @@ -17871,6 +17881,7 @@ impl<'a> Parser<'a> { opt_except, opt_rename, opt_replace, + opt_alias, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a3b5404d3..3f0ca96a0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1280,6 +1280,26 @@ fn parse_select_expr_star() { dialects.verified_only_select("SELECT myfunc().* EXCEPT (foo) FROM T"); } +#[test] +fn parse_select_wildcard_with_alias() { + let dialects = all_dialects_where(|d| d.supports_select_wildcard_with_alias()); + + // qualified wildcard with alias + dialects + .parse_sql_statements("SELECT t.* AS all_cols FROM t") + .unwrap(); + + // unqualified wildcard with alias + dialects + .parse_sql_statements("SELECT * AS all_cols FROM t") + .unwrap(); + + // mixed: regular column + qualified wildcard with alias + dialects + .parse_sql_statements("SELECT a.id, b.* AS b_cols FROM a JOIN b ON (a.id = b.a_id)") + .unwrap(); +} + #[test] fn test_eof_after_as() { let res = parse_sql_statements("SELECT foo AS"); From e87241a153609f0d4716954797b24e205424b945 Mon Sep 17 00:00:00 2001 From: Yoabot Date: Thu, 26 Feb 2026 12:00:53 +0100 Subject: [PATCH 02/10] Snowflake: support wildcard with EXCLUDE in function arguments (#2231) --- src/ast/mod.rs | 5 +++++ src/ast/spans.rs | 2 ++ src/parser/mod.rs | 21 ++++++++++++++++++++- tests/sqlparser_common.rs | 16 ++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7b1e9447d..1e430171e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7625,6 +7625,10 @@ pub enum FunctionArgExpr { QualifiedWildcard(ObjectName), /// An unqualified `*` wildcard. Wildcard, + /// An unqualified `*` wildcard with additional options, e.g. `* EXCLUDE(col)`. + /// + /// Used in Snowflake to support expressions like `HASH(* EXCLUDE(col))`. + WildcardWithOptions(WildcardAdditionalOptions), } impl From for FunctionArgExpr { @@ -7643,6 +7647,7 @@ impl fmt::Display for FunctionArgExpr { FunctionArgExpr::Expr(expr) => write!(f, "{expr}"), FunctionArgExpr::QualifiedWildcard(prefix) => write!(f, "{prefix}.*"), FunctionArgExpr::Wildcard => f.write_str("*"), + FunctionArgExpr::WildcardWithOptions(opts) => write!(f, "*{opts}"), } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1e2162080..0b95c3ed7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2130,6 +2130,7 @@ impl Spanned for FunctionArg { /// /// Missing spans: /// - [FunctionArgExpr::Wildcard] +/// - [FunctionArgExpr::WildcardWithOptions] impl Spanned for FunctionArgExpr { fn span(&self) -> Span { match self { @@ -2138,6 +2139,7 @@ impl Spanned for FunctionArgExpr { union_spans(object_name.0.iter().map(|i| i.span())) } FunctionArgExpr::Wildcard => Span::empty(), + FunctionArgExpr::WildcardWithOptions(_) => Span::empty(), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 667a15352..bea566bbe 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17576,7 +17576,26 @@ impl<'a> Parser<'a> { if let Some(arg) = arg { return Ok(arg); } - Ok(FunctionArg::Unnamed(self.parse_wildcard_expr()?.into())) + let wildcard_expr = self.parse_wildcard_expr()?; + let arg_expr: FunctionArgExpr = match wildcard_expr { + Expr::Wildcard(ref token) if self.dialect.supports_select_wildcard_exclude() => { + // Support `* EXCLUDE(col1, col2, ...)` inside function calls (e.g. Snowflake's + // `HASH(* EXCLUDE(col))`). Parse the options the same way SELECT items do. + let opts = self.parse_wildcard_additional_options(token.0.clone())?; + if opts.opt_exclude.is_some() + || opts.opt_except.is_some() + || opts.opt_replace.is_some() + || opts.opt_rename.is_some() + || opts.opt_ilike.is_some() + { + FunctionArgExpr::WildcardWithOptions(opts) + } else { + wildcard_expr.into() + } + } + other => other.into(), + }; + Ok(FunctionArg::Unnamed(arg_expr)) } fn parse_function_named_arg_operator(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3f0ca96a0..982bf1088 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18583,3 +18583,19 @@ fn parse_array_subscript() { dialects.verified_stmt("SELECT arr[1][2]"); dialects.verified_stmt("SELECT arr[:][:]"); } + +#[test] +fn test_wildcard_func_arg() { + // Wildcard (*) and wildcard with EXCLUDE as a function argument. + // Documented for Snowflake's HASH function but parsed for any dialect that + // supports the wildcard-EXCLUDE select syntax. + let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude()); + + // Wildcard with EXCLUDE — canonical form has a space before the parenthesised column list. + dialects.one_statement_parses_to( + "SELECT HASH(* EXCLUDE(col1)) FROM t", + "SELECT HASH(* EXCLUDE (col1)) FROM t", + ); + dialects.verified_expr("HASH(* EXCLUDE (col1))"); + dialects.verified_expr("HASH(* EXCLUDE (col1, col2))"); +} From bd7f70e82048cab324dba6224fb3dd17757d8477 Mon Sep 17 00:00:00 2001 From: Yoabot Date: Thu, 26 Feb 2026 12:11:53 +0100 Subject: [PATCH 03/10] MSSQL: prevent statement-starting keywords from being consumed as implicit aliases (#2233) --- src/dialect/mssql.rs | 56 ++++++++++++++++++++++++++++++++++++---- tests/sqlparser_mssql.rs | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 42e05858f..8ad765dd3 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -129,9 +129,30 @@ impl Dialect for MsSqlDialect { fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { match kw { - // List of keywords that cannot be used as select item aliases in MSSQL - // regardless of whether the alias is explicit or implicit - Keyword::IF | Keyword::ELSE => false, + // List of keywords that cannot be used as select item (column) aliases in MSSQL + // regardless of whether the alias is explicit or implicit. + // + // These are T-SQL statement-starting keywords; allowing them as implicit aliases + // causes the parser to consume the keyword as an alias for the previous expression, + // then fail on the token that follows (e.g. `TABLE`, `@var`, `sp_name`, …). + Keyword::IF + | Keyword::ELSE + | Keyword::DECLARE + | Keyword::EXEC + | Keyword::EXECUTE + | Keyword::INSERT + | Keyword::UPDATE + | Keyword::DELETE + | Keyword::DROP + | Keyword::CREATE + | Keyword::ALTER + | Keyword::TRUNCATE + | Keyword::PRINT + | Keyword::WHILE + | Keyword::RETURN + | Keyword::THROW + | Keyword::RAISERROR + | Keyword::MERGE => false, _ => explicit || self.is_column_alias(kw, parser), } } @@ -139,8 +160,33 @@ impl Dialect for MsSqlDialect { fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { match kw { // List of keywords that cannot be used as table aliases in MSSQL - // regardless of whether the alias is explicit or implicit - Keyword::IF | Keyword::ELSE => false, + // regardless of whether the alias is explicit or implicit. + // + // These are T-SQL statement-starting keywords. Without blocking them here, + // a bare `SELECT * FROM t` followed by a newline and one of these keywords + // would cause the parser to consume the keyword as a table alias for `t`, + // then fail on the token that follows (e.g. `@var`, `sp_name`, `TABLE`, …). + // + // `SET` is already covered by the global `RESERVED_FOR_TABLE_ALIAS` list; + // the keywords below are MSSQL-specific additions. + Keyword::IF + | Keyword::ELSE + | Keyword::DECLARE + | Keyword::EXEC + | Keyword::EXECUTE + | Keyword::INSERT + | Keyword::UPDATE + | Keyword::DELETE + | Keyword::DROP + | Keyword::CREATE + | Keyword::ALTER + | Keyword::TRUNCATE + | Keyword::PRINT + | Keyword::WHILE + | Keyword::RETURN + | Keyword::THROW + | Keyword::RAISERROR + | Keyword::MERGE => false, _ => explicit || self.is_table_alias(kw, parser), } } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b5fd1e77e..6c8412a4a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2730,3 +2730,56 @@ fn parse_mssql_tran_shorthand() { // ROLLBACK TRAN normalizes to ROLLBACK (same as ROLLBACK TRANSACTION) ms().one_statement_parses_to("ROLLBACK TRAN", "ROLLBACK"); } + +#[test] +fn test_tsql_statement_keywords_not_implicit_aliases() { + // T-SQL statement-starting keywords must never be consumed as implicit + // aliases for a preceding SELECT item or table reference when using + // newline-delimited multi-statement scripts. + + // Without the fix, the parser would consume a statement-starting keyword + // as an implicit alias for the preceding SELECT item or table reference, + // then fail on the next token. Verify parsing succeeds and each input + // produces the expected number of statements. + + // Keywords that should not become implicit column aliases + let col_alias_cases: &[(&str, usize)] = &[ + ("select 1\ndeclare @x as int", 2), + ("select 1\nexec sp_who", 2), + ("select 1\ninsert into t values (1)", 2), + ("select 1\nupdate t set col=1", 2), + ("select 1\ndelete from t", 2), + ("select 1\ndrop table t", 2), + ("select 1\ncreate table t (id int)", 2), + ("select 1\nalter table t add col int", 2), + ("select 1\nreturn", 2), + ]; + for (sql, expected) in col_alias_cases { + let stmts = tsql() + .parse_sql_statements(sql) + .unwrap_or_else(|e| panic!("failed to parse {sql:?}: {e}")); + assert_eq!( + stmts.len(), + *expected, + "expected {expected} stmts for: {sql:?}" + ); + } + + // Keywords that should not become implicit table aliases + let tbl_alias_cases: &[(&str, usize)] = &[ + ("select * from t\ndeclare @x as int", 2), + ("select * from t\ndrop table t", 2), + ("select * from t\ncreate table u (id int)", 2), + ("select * from t\nexec sp_who", 2), + ]; + for (sql, expected) in tbl_alias_cases { + let stmts = tsql() + .parse_sql_statements(sql) + .unwrap_or_else(|e| panic!("failed to parse {sql:?}: {e}")); + assert_eq!( + stmts.len(), + *expected, + "expected {expected} stmts for: {sql:?}" + ); + } +} From 523d78ea2aceee54c4f999728edcb5603ff25a24 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 27 Feb 2026 11:33:53 +0100 Subject: [PATCH 04/10] Support parenthesized `CREATE TABLE ... (LIKE ... INCLUDING/EXCLUDING DEFAULTS)` in `PostgreSQL` (#2242) --- src/dialect/postgresql.rs | 4 ++++ tests/sqlparser_postgres.rs | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 0b7ed2a72..89b677c47 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -302,4 +302,8 @@ impl Dialect for PostgreSqlDialect { fn supports_insert_table_alias(&self) -> bool { true } + + fn supports_create_table_like_parenthesized(&self) -> bool { + true + } } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7c19f51e5..f4b3a2826 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -593,6 +593,45 @@ fn parse_create_table_constraints_only() { }; } +#[test] +fn parse_create_table_like_with_defaults() { + let sql = "CREATE TABLE new (LIKE old INCLUDING DEFAULTS)"; + match pg().verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: Some(CreateTableLikeDefaults::Including), + })) + ) + } + _ => unreachable!(), + } + + let sql = "CREATE TABLE new (LIKE old EXCLUDING DEFAULTS)"; + match pg().verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: Some(CreateTableLikeDefaults::Excluding), + })) + ) + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table_constraints_rename() { match alter_table_op( From 6f0e803aa77414e83aefd326e23231c51b60ae32 Mon Sep 17 00:00:00 2001 From: Yoabot Date: Fri, 27 Feb 2026 11:44:59 +0100 Subject: [PATCH 05/10] MSSQL: support EXEC (@sql) dynamic SQL execution (#2234) --- src/parser/mod.rs | 23 ++++++++++++++++++++--- tests/sqlparser_mssql.rs | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bea566bbe..75db4d240 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18583,6 +18583,9 @@ impl<'a> Parser<'a> { /// Parse a SQL `EXECUTE` statement pub fn parse_execute(&mut self) -> Result { + // Track whether the procedure/expression name itself was wrapped in parens, + // i.e. `EXEC (@sql)` (dynamic string execution) vs `EXEC sp_name`. + // When the name has parens there are no additional parameters. let name = if self.dialect.supports_execute_immediate() && self.parse_keyword(Keyword::IMMEDIATE) { @@ -18593,10 +18596,18 @@ impl<'a> Parser<'a> { if has_parentheses { self.expect_token(&Token::RParen)?; } - Some(name) + Some((name, has_parentheses)) }; - let has_parentheses = self.consume_token(&Token::LParen); + let name_had_parentheses = name.as_ref().map(|(_, p)| *p).unwrap_or(false); + + // Only look for a parameter list when the name was NOT wrapped in parens. + // `EXEC (@sql)` is dynamic SQL execution and takes no parameters here. + let has_parentheses = if name_had_parentheses { + false + } else { + self.consume_token(&Token::LParen) + }; let end_kws = &[Keyword::USING, Keyword::OUTPUT, Keyword::DEFAULT]; let end_token = match (has_parentheses, self.peek_token().token) { @@ -18606,12 +18617,18 @@ impl<'a> Parser<'a> { (false, _) => Token::SemiColon, }; - let parameters = self.parse_comma_separated0(Parser::parse_expr, end_token)?; + let parameters = if name_had_parentheses { + vec![] + } else { + self.parse_comma_separated0(Parser::parse_expr, end_token)? + }; if has_parentheses { self.expect_token(&Token::RParen)?; } + let name = name.map(|(n, _)| n); + let into = if self.parse_keyword(Keyword::INTO) { self.parse_comma_separated(Self::parse_identifier)? } else { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 6c8412a4a..8bdb1c205 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2783,3 +2783,26 @@ fn test_tsql_statement_keywords_not_implicit_aliases() { ); } } + +#[test] +fn test_exec_dynamic_sql() { + // EXEC (@sql) executes a dynamic SQL string held in a variable. + // It must parse as a single Execute statement and not attempt to parse + // parameters after the closing paren. + let stmts = tsql() + .parse_sql_statements("EXEC (@sql)") + .expect("EXEC (@sql) should parse"); + assert_eq!(stmts.len(), 1); + assert!( + matches!(&stmts[0], Statement::Execute { .. }), + "expected Execute, got: {:?}", + stmts[0] + ); + + // Verify that a statement following EXEC (@sql) on the next line is parsed + // as a separate statement and not consumed as a parameter. + let stmts = tsql() + .parse_sql_statements("EXEC (@sql)\nDROP TABLE #tmp") + .expect("EXEC (@sql) followed by DROP TABLE should parse"); + assert_eq!(stmts.len(), 2); +} From 83baf5e89179dd00321330312fe460100d755c25 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 27 Feb 2026 02:59:05 -0800 Subject: [PATCH 06/10] Support MySQL KEY keyword in column definitions (#2243) Co-authored-by: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> --- src/ast/ddl.rs | 2 +- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 12 ++++++++++++ src/dialect/mysql.rs | 5 +++++ src/parser/mod.rs | 23 ++++++++++++++++++++++- tests/sqlparser_mysql.rs | 9 +++++++++ 6 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0c4f93e64..3a951f66b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2040,7 +2040,7 @@ impl fmt::Display for ColumnOption { Ok(()) } Unique(constraint) => { - write!(f, "UNIQUE")?; + write!(f, "UNIQUE{:>}", constraint.index_type_display)?; if let Some(characteristics) = &constraint.characteristics { write!(f, " {characteristics}")?; } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 1cf195e63..a7a3c2715 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -280,4 +280,8 @@ impl Dialect for GenericDialect { fn supports_constraint_keyword_without_name(&self) -> bool { true } + + fn supports_key_column_option(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 698c12ec9..796b25f05 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1195,6 +1195,18 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports the `KEY` keyword as part of + /// column-level constraints in a `CREATE TABLE` statement. + /// + /// When enabled, the parser accepts these MySQL-specific column options: + /// - `UNIQUE [KEY]` — optional `KEY` after `UNIQUE` + /// - `[PRIMARY] KEY` — standalone `KEY` as shorthand for `PRIMARY KEY` + /// + /// + fn supports_key_column_option(&self) -> bool { + false + } + /// Returns true if the specified keyword is reserved and cannot be /// used as an identifier without special handling like quoting. fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index bdced4826..6b057539e 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -206,6 +206,11 @@ impl Dialect for MySqlDialect { fn supports_constraint_keyword_without_name(&self) -> bool { true } + + /// See: + fn supports_key_column_option(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 75db4d240..a00eab348 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9048,12 +9048,18 @@ impl<'a> Parser<'a> { .into(), )) } else if self.parse_keyword(Keyword::UNIQUE) { + let index_type_display = + if self.dialect.supports_key_column_option() && self.parse_keyword(Keyword::KEY) { + KeyOrIndexDisplay::Key + } else { + KeyOrIndexDisplay::None + }; let characteristics = self.parse_constraint_characteristics()?; Ok(Some( UniqueConstraint { name: None, index_name: None, - index_type_display: KeyOrIndexDisplay::None, + index_type_display, index_type: None, columns: vec![], index_options: vec![], @@ -9062,6 +9068,21 @@ impl<'a> Parser<'a> { } .into(), )) + } else if self.dialect.supports_key_column_option() && self.parse_keyword(Keyword::KEY) { + // In MySQL, `KEY` in a column definition is shorthand for `PRIMARY KEY`. + // See: https://dev.mysql.com/doc/refman/8.4/en/create-table.html + let characteristics = self.parse_constraint_characteristics()?; + Ok(Some( + PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics, + } + .into(), + )) } else if self.parse_keyword(Keyword::REFERENCES) { let foreign_table = self.parse_object_name(false)?; // PostgreSQL allows omitting the column list and diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 30405623d..b4ae764c2 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -944,6 +944,15 @@ fn parse_create_table_primary_and_unique_key_characteristic_test() { } } +#[test] +fn parse_create_table_column_key_options() { + mysql_and_generic().verified_stmt("CREATE TABLE foo (x INT UNIQUE KEY)"); + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE foo (x INT KEY)", + "CREATE TABLE foo (x INT PRIMARY KEY)", + ); +} + #[test] fn parse_create_table_comment() { let without_equal = "CREATE TABLE foo (bar INT) COMMENT 'baz'"; From 5b7bc1a52723cc91d4d0b90a44455d716db549d5 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 27 Feb 2026 13:38:54 +0100 Subject: [PATCH 07/10] Support two-argument `TRIM(string, characters)` in PostgreSQL (#2240) --- src/ast/mod.rs | 2 +- src/dialect/bigquery.rs | 4 +++ src/dialect/clickhouse.rs | 4 +++ src/dialect/duckdb.rs | 4 +++ src/dialect/generic.rs | 4 +++ src/dialect/mod.rs | 6 +++++ src/dialect/postgresql.rs | 4 +++ src/dialect/snowflake.rs | 4 +++ src/dialect/sqlite.rs | 4 +++ src/parser/mod.rs | 5 ++-- tests/sqlparser_common.rs | 53 ++++++++++++++++++++++++++++----------- 11 files changed, 75 insertions(+), 19 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1e430171e..97cc61935 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1131,7 +1131,7 @@ pub enum Expr { /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) /// TRIM() - /// TRIM(, [, characters]) -- only Snowflake or Bigquery + /// TRIM(, [, characters]) -- PostgreSQL, DuckDB, Snowflake, BigQuery, Generic /// ``` Trim { /// The expression to trim from. diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 6cef46067..8fca51518 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -162,4 +162,8 @@ impl Dialect for BigQueryDialect { fn supports_select_wildcard_replace(&self) -> bool { true } + + fn supports_comma_separated_trim(&self) -> bool { + true + } } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index ea4d7a971..87c762f0b 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -141,4 +141,8 @@ impl Dialect for ClickHouseDialect { fn supports_select_wildcard_replace(&self) -> bool { true } + + fn supports_comma_separated_trim(&self) -> bool { + true + } } diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 32967c4c5..e70efd695 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -129,4 +129,8 @@ impl Dialect for DuckDbDialect { fn supports_select_wildcard_replace(&self) -> bool { true } + + fn supports_comma_separated_trim(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index a7a3c2715..1d5461fec 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -284,4 +284,8 @@ impl Dialect for GenericDialect { fn supports_key_column_option(&self) -> bool { true } + + fn supports_comma_separated_trim(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 796b25f05..8703e402c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1651,6 +1651,12 @@ pub trait Dialect: Debug + Any { fn supports_select_format(&self) -> bool { false } + + /// Returns true if the dialect supports the two-argument comma-separated + /// form of the `TRIM` function: `TRIM(expr, characters)`. + fn supports_comma_separated_trim(&self) -> bool { + false + } } /// Operators for which precedence must be defined. diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 89b677c47..b99a8b5c3 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -306,4 +306,8 @@ impl Dialect for PostgreSqlDialect { fn supports_create_table_like_parenthesized(&self) -> bool { true } + + fn supports_comma_separated_trim(&self) -> bool { + true + } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 984e384fd..a9d71fc4b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -667,6 +667,10 @@ impl Dialect for SnowflakeDialect { fn supports_lambda_functions(&self) -> bool { true } + + fn supports_comma_separated_trim(&self) -> bool { + true + } } // Peeks ahead to identify tokens that are expected after diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index b44a1c5b8..39ee622d8 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -120,4 +120,8 @@ impl Dialect for SQLiteDialect { fn supports_notnull_operator(&self) -> bool { true } + + fn supports_comma_separated_trim(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a00eab348..8d8b55a34 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2940,7 +2940,7 @@ impl<'a> Parser<'a> { /// ```sql /// TRIM ([WHERE] ['text' FROM] 'text') /// TRIM ('text') - /// TRIM(, [, characters]) -- only Snowflake or BigQuery + /// TRIM(, [, characters]) -- PostgreSQL, DuckDB, Snowflake, BigQuery, Generic /// ``` pub fn parse_trim_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; @@ -2961,8 +2961,7 @@ impl<'a> Parser<'a> { trim_what: Some(trim_what), trim_characters: None, }) - } else if self.consume_token(&Token::Comma) - && dialect_of!(self is DuckDbDialect | SnowflakeDialect | BigQueryDialect | GenericDialect) + } else if self.dialect.supports_comma_separated_trim() && self.consume_token(&Token::Comma) { let characters = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 982bf1088..8de460d78 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8099,23 +8099,46 @@ fn parse_trim() { parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')").unwrap_err() ); - //keep Snowflake/BigQuery TRIM syntax failing - let all_expected_snowflake = TestedDialects::new(vec![ - //Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(AnsiDialect {}), - //Box::new(SnowflakeDialect {}), - Box::new(HiveDialect {}), - Box::new(RedshiftSqlDialect {}), - Box::new(MySqlDialect {}), - //Box::new(BigQueryDialect {}), - Box::new(SQLiteDialect {}), - ]); + // dialects that support comma-separated TRIM syntax + let dialects = all_dialects_where(|d| d.supports_comma_separated_trim()); + let sql = "SELECT TRIM(' xyz ', ' ')"; + let select = dialects.verified_only_select(sql); assert_eq!( - ParserError::ParserError("Expected: ), found: 'a'".to_owned()), - all_expected_snowflake + &Expr::Trim { + expr: Box::new(Expr::Value( + Value::SingleQuotedString(" xyz ".to_owned()).with_empty_span() + )), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value( + Value::SingleQuotedString(" ".to_owned()).with_empty_span() + )]), + }, + expr_from_projection(only(&select.projection)) + ); + + let sql = "SELECT TRIM('xyz', 'a')"; + let select = dialects.verified_only_select(sql); + assert_eq!( + &Expr::Trim { + expr: Box::new(Expr::Value( + Value::SingleQuotedString("xyz".to_owned()).with_empty_span() + )), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value( + Value::SingleQuotedString("a".to_owned()).with_empty_span() + )]), + }, + expr_from_projection(only(&select.projection)) + ); + + // dialects without comma-style TRIM syntax should fail + let unsupported_dialects = all_dialects_where(|d| !d.supports_comma_separated_trim()); + assert_eq!( + ParserError::ParserError("Expected: ), found: ,".to_owned()), + unsupported_dialects .parse_sql_statements("SELECT TRIM('xyz', 'a')") .unwrap_err() ); From 2b5e91137c003526d58625bdaa459b68d3eab28a Mon Sep 17 00:00:00 2001 From: Yoa Bot Date: Thu, 26 Feb 2026 11:50:59 +0100 Subject: [PATCH 08/10] 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` --- src/ast/query.rs | 4 +-- src/ast/spans.rs | 4 +-- src/parser/mod.rs | 6 +++-- tests/sqlparser_common.rs | 47 +++++++++++++++++++++++++++++++----- tests/sqlparser_duckdb.rs | 12 ++++++--- tests/sqlparser_snowflake.rs | 16 ++++++++---- 6 files changed, 68 insertions(+), 21 deletions(-) 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 a00eab348..3adad0abf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17952,11 +17952,13 @@ 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 982bf1088..d439d8181 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17320,7 +17320,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!(), @@ -17333,8 +17335,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")), ])) ); } @@ -17345,7 +17347,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!(), @@ -17367,7 +17371,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| { @@ -17378,7 +17384,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!(), @@ -17415,6 +17423,33 @@ 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"), From a3cfcac3a1c2f336e39daa08a3113d03628b6c8a Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 27 Feb 2026 05:47:28 -0800 Subject: [PATCH 09/10] Add Readyset to users in README.md (#2247) --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9dfe50810..775d07491 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,9 @@ $ cargo run --features json_example --example cli FILENAME.sql [--dialectname] ## Users -This parser is currently being used by the [DataFusion] query engine, [LocustDB], -[Ballista], [GlueSQL], [Opteryx], [Polars], [PRQL], [Qrlew], [JumpWire], [ParadeDB], [CipherStash Proxy], -and [GreptimeDB]. +This parser is currently being used by the [DataFusion] query engine, +[LocustDB], [Ballista], [GlueSQL], [Opteryx], [Polars], [PRQL], [Qrlew], +[JumpWire], [ParadeDB], [CipherStash Proxy], [Readyset] and [GreptimeDB]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. @@ -282,3 +282,4 @@ licensed as above, without any additional terms or conditions. [`GenericDialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/struct.GenericDialect.html [CipherStash Proxy]: https://github.com/cipherstash/proxy [GreptimeDB]: https://github.com/GreptimeTeam/greptimedb +[Readyset]: https://github.com/readysettech/readyset From a640e271cf80c7e9fc453bd9c1b86a50fe7ba4e9 Mon Sep 17 00:00:00 2001 From: Yoa Bot Date: Fri, 27 Feb 2026 14:50:18 +0100 Subject: [PATCH 10/10] chore: apply cargo fmt --- src/parser/mod.rs | 5 ++--- tests/sqlparser_common.rs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3adad0abf..4b8973198 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17952,9 +17952,8 @@ 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_object_name(false) - })?; + let columns = + self.parse_comma_separated(|parser| parser.parse_object_name(false))?; self.expect_token(&Token::RParen)?; Some(ExcludeSelectItem::Multiple(columns)) } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d439d8181..51c1c6ec9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17429,9 +17429,8 @@ fn test_select_exclude_qualified_names() { 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", - ); + 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!(