From a629ddf89b0fbfb189d07b3130bc34aad9b9faf3 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 28 Feb 2025 22:07:39 -0800 Subject: [PATCH 001/130] Ignore escaped LIKE wildcards in MySQL (#1735) --- src/dialect/mod.rs | 27 ++++++++++++++++++++++++++ src/dialect/mysql.rs | 4 ++++ src/tokenizer.rs | 22 +++++++++++++++++++-- tests/sqlparser_common.rs | 40 ++++++++++++++++++++++++++------------- tests/sqlparser_mysql.rs | 11 +++++++++++ 5 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 1c32bc51..1cea6bc2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -201,6 +201,33 @@ pub trait Dialect: Debug + Any { false } + /// Determine whether the dialect strips the backslash when escaping LIKE wildcards (%, _). + /// + /// [MySQL] has a special case when escaping single quoted strings which leaves these unescaped + /// so they can be used in LIKE patterns without double-escaping (as is necessary in other + /// escaping dialects, such as [Snowflake]). Generally, special characters have escaping rules + /// causing them to be replaced with a different byte sequences (e.g. `'\0'` becoming the zero + /// byte), and the default if an escaped character does not have a specific escaping rule is to + /// strip the backslash (e.g. there is no rule for `h`, so `'\h' = 'h'`). MySQL's special case + /// for ignoring LIKE wildcard escapes is to *not* strip the backslash, so that `'\%' = '\\%'`. + /// This applies to all string literals though, not just those used in LIKE patterns. + /// + /// ```text + /// mysql> select '\_', hex('\\'), hex('_'), hex('\_'); + /// +----+-----------+----------+-----------+ + /// | \_ | hex('\\') | hex('_') | hex('\_') | + /// +----+-----------+----------+-----------+ + /// | \_ | 5C | 5F | 5C5F | + /// +----+-----------+----------+-----------+ + /// 1 row in set (0.00 sec) + /// ``` + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/functions/like#usage-notes + fn ignores_wildcard_escapes(&self) -> bool { + false + } + /// Determine if the dialect supports string literals with `U&` prefix. /// This is used to specify Unicode code points in string literals. /// For example, in PostgreSQL, the following is a valid string literal: diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 8a0da87e..cb86f2b4 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -62,6 +62,10 @@ impl Dialect for MySqlDialect { true } + fn ignores_wildcard_escapes(&self) -> bool { + true + } + fn supports_numeric_prefix(&self) -> bool { true } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index bc0f0efe..d33a7d8a 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -2011,8 +2011,13 @@ impl<'a> Tokenizer<'a> { num_consecutive_quotes = 0; if let Some(next) = chars.peek() { - if !self.unescape { - // In no-escape mode, the given query has to be saved completely including backslashes. + if !self.unescape + || (self.dialect.ignores_wildcard_escapes() + && (*next == '%' || *next == '_')) + { + // In no-escape mode, the given query has to be saved completely + // including backslashes. Similarly, with ignore_like_wildcard_escapes, + // the backslash is not stripped. s.push(ch); s.push(*next); chars.next(); // consume next @@ -3585,6 +3590,9 @@ mod tests { (r#"'\\a\\b\'c'"#, r#"\\a\\b\'c"#, r#"\a\b'c"#), (r#"'\'abcd'"#, r#"\'abcd"#, r#"'abcd"#), (r#"'''a''b'"#, r#"''a''b"#, r#"'a'b"#), + (r#"'\q'"#, r#"\q"#, r#"q"#), + (r#"'\%\_'"#, r#"\%\_"#, r#"%_"#), + (r#"'\\%\\_'"#, r#"\\%\\_"#, r#"\%\_"#), ] { let tokens = Tokenizer::new(&dialect, sql) .with_unescape(false) @@ -3618,6 +3626,16 @@ mod tests { compare(expected, tokens); } + + // MySQL special case for LIKE escapes + for (sql, expected) in [(r#"'\%'"#, r#"\%"#), (r#"'\_'"#, r#"\_"#)] { + let dialect = MySqlDialect {}; + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + + let expected = vec![Token::SingleQuotedString(expected.to_string())]; + + compare(expected, tokens); + } } #[test] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0a68d31e..3c43ed61 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10387,15 +10387,8 @@ fn parse_with_recursion_limit() { #[test] fn parse_escaped_string_with_unescape() { - fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = TestedDialects::new(vec![ - Box::new(MySqlDialect {}), - Box::new(BigQueryDialect {}), - Box::new(SnowflakeDialect {}), - ]) - .one_statement_parses_to(sql, ""); - - match stmt { + fn assert_mysql_query_value(dialects: &TestedDialects, sql: &str, quoted: &str) { + match dialects.one_statement_parses_to(sql, "") { Statement::Query(query) => match *query.body { SetExpr::Select(value) => { let expr = expr_from_projection(only(&value.projection)); @@ -10411,17 +10404,38 @@ fn parse_escaped_string_with_unescape() { _ => unreachable!(), }; } + + let escaping_dialects = + &all_dialects_where(|dialect| dialect.supports_string_literal_backslash_escape()); + let no_wildcard_exception = &all_dialects_where(|dialect| { + dialect.supports_string_literal_backslash_escape() && !dialect.ignores_wildcard_escapes() + }); + let with_wildcard_exception = &all_dialects_where(|dialect| { + dialect.supports_string_literal_backslash_escape() && dialect.ignores_wildcard_escapes() + }); + let sql = r"SELECT 'I\'m fine'"; - assert_mysql_query_value(sql, "I'm fine"); + assert_mysql_query_value(escaping_dialects, sql, "I'm fine"); let sql = r#"SELECT 'I''m fine'"#; - assert_mysql_query_value(sql, "I'm fine"); + assert_mysql_query_value(escaping_dialects, sql, "I'm fine"); let sql = r#"SELECT 'I\"m fine'"#; - assert_mysql_query_value(sql, "I\"m fine"); + assert_mysql_query_value(escaping_dialects, sql, "I\"m fine"); let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \h \ '"; - assert_mysql_query_value(sql, "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} \u{7} h "); + assert_mysql_query_value( + no_wildcard_exception, + sql, + "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} \u{7} h ", + ); + + // check MySQL doesn't remove backslash from escaped LIKE wildcards + assert_mysql_query_value( + with_wildcard_exception, + sql, + "Testing: \0 \\ \\% \\_ \u{8} \n \r \t \u{1a} \u{7} h ", + ); } #[test] diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 15f79b4c..f0774fcf 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2627,6 +2627,17 @@ fn parse_rlike_and_regexp() { } } +#[test] +fn parse_like_with_escape() { + // verify backslash is not stripped for escaped wildcards + mysql().verified_only_select(r#"SELECT 'a\%c' LIKE 'a\%c'"#); + mysql().verified_only_select(r#"SELECT 'a\_c' LIKE 'a\_c'"#); + mysql().verified_only_select(r#"SELECT '%\_\%' LIKE '%\_\%'"#); + mysql().verified_only_select(r#"SELECT '\_\%' LIKE CONCAT('\_', '\%')"#); + mysql().verified_only_select(r#"SELECT 'a%c' LIKE 'a$%c' ESCAPE '$'"#); + mysql().verified_only_select(r#"SELECT 'a_c' LIKE 'a#_c' ESCAPE '#'"#); +} + #[test] fn parse_kill() { let stmt = mysql_and_generic().verified_stmt("KILL CONNECTION 5"); From 9e09b617e89b20b8c1f3ddccee35e1b04ac77188 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 28 Feb 2025 22:12:25 -0800 Subject: [PATCH 002/130] Parse SET NAMES syntax in Postgres (#1752) --- src/ast/mod.rs | 7 ++----- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 10 ++++++++++ src/dialect/mysql.rs | 4 ++++ src/dialect/postgresql.rs | 4 ++++ src/parser/mod.rs | 8 ++++---- tests/sqlparser_common.rs | 8 ++++++++ tests/sqlparser_mysql.rs | 6 +++--- 8 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 72be3ff6..554ec19b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2956,10 +2956,8 @@ pub enum Statement { /// ```sql /// SET NAMES 'charset_name' [COLLATE 'collation_name'] /// ``` - /// - /// Note: this is a MySQL-specific statement. SetNames { - charset_name: String, + charset_name: Ident, collation_name: Option, }, /// ```sql @@ -4684,8 +4682,7 @@ impl fmt::Display for Statement { charset_name, collation_name, } => { - f.write_str("SET NAMES ")?; - f.write_str(charset_name)?; + write!(f, "SET NAMES {}", charset_name)?; if let Some(collation) = collation_name { f.write_str(" COLLATE ")?; diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 041d44bb..c13d5aa6 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -155,4 +155,8 @@ impl Dialect for GenericDialect { fn supports_match_against(&self) -> bool { true } + + fn supports_set_names(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 1cea6bc2..aeb097cf 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -980,6 +980,16 @@ pub trait Dialect: Debug + Any { fn supports_order_by_all(&self) -> bool { false } + + /// Returns true if the dialect supports `SET NAMES [COLLATE ]`. + /// + /// - [MySQL](https://dev.mysql.com/doc/refman/8.4/en/set-names.html) + /// - [Postgres](https://www.postgresql.org/docs/17/sql-set.html) + /// + /// Note: Postgres doesn't support the `COLLATE` clause, but we permissively parse it anyway. + fn supports_set_names(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index cb86f2b4..0bdfc9bf 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -137,6 +137,10 @@ impl Dialect for MySqlDialect { fn supports_match_against(&self) -> bool { true } + + fn supports_set_names(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 57ed0b68..9b08b8f3 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -254,4 +254,8 @@ impl Dialect for PostgreSqlDialect { fn supports_geometric_types(&self) -> bool { true } + + fn supports_set_names(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f234fcc0..b11e5779 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10962,14 +10962,14 @@ impl<'a> Parser<'a> { OneOrManyWithParens::One(self.parse_object_name(false)?) }; - if matches!(&variables, OneOrManyWithParens::One(variable) if variable.to_string().eq_ignore_ascii_case("NAMES") - && dialect_of!(self is MySqlDialect | GenericDialect)) - { + let names = matches!(&variables, OneOrManyWithParens::One(variable) if variable.to_string().eq_ignore_ascii_case("NAMES")); + + if names && self.dialect.supports_set_names() { if self.parse_keyword(Keyword::DEFAULT) { return Ok(Statement::SetNamesDefault {}); } - let charset_name = self.parse_literal_string()?; + let charset_name = self.parse_identifier()?; let collation_name = if self.parse_one_of_keywords(&[Keyword::COLLATE]).is_some() { Some(self.parse_literal_string()?) } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3c43ed61..2c35c243 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14631,3 +14631,11 @@ fn parse_array_type_def_with_brackets() { dialects.verified_stmt("SELECT x::INT[]"); dialects.verified_stmt("SELECT STRING_TO_ARRAY('1,2,3', ',')::INT[3]"); } + +#[test] +fn parse_set_names() { + let dialects = all_dialects_where(|d| d.supports_set_names()); + dialects.verified_stmt("SET NAMES 'UTF8'"); + dialects.verified_stmt("SET NAMES 'utf8'"); + dialects.verified_stmt("SET NAMES UTF8 COLLATE bogus"); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f0774fcf..8d89ce4e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2696,7 +2696,7 @@ fn parse_set_names() { assert_eq!( stmt, Statement::SetNames { - charset_name: "utf8mb4".to_string(), + charset_name: "utf8mb4".into(), collation_name: None, } ); @@ -2705,7 +2705,7 @@ fn parse_set_names() { assert_eq!( stmt, Statement::SetNames { - charset_name: "utf8mb4".to_string(), + charset_name: "utf8mb4".into(), collation_name: Some("bogus".to_string()), } ); @@ -2716,7 +2716,7 @@ fn parse_set_names() { assert_eq!( stmt, vec![Statement::SetNames { - charset_name: "utf8mb4".to_string(), + charset_name: "utf8mb4".into(), collation_name: Some("bogus".to_string()), }] ); From d5dbe86da9c0591d5c2bf573b641d539f04e3028 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Sat, 1 Mar 2025 07:13:33 +0100 Subject: [PATCH 003/130] re-add support for nested comments in mssql (#1754) --- src/dialect/mssql.rs | 5 +++++ tests/sqlparser_mssql.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 980f5ec3..aeed1eb7 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -95,4 +95,9 @@ impl Dialect for MsSqlDialect { fn supports_timestamp_versioning(&self) -> bool { true } + + /// See + fn supports_nested_comments(&self) -> bool { + true + } } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ec565e50..3f313af4 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1620,6 +1620,22 @@ fn parse_create_table_with_valid_options() { } } +#[test] +fn parse_nested_slash_star_comment() { + let sql = r#" + select + /* + comment level 1 + /* + comment level 2 + */ + */ + 1; + "#; + let canonical = "SELECT 1"; + ms().one_statement_parses_to(sql, canonical); +} + #[test] fn parse_create_table_with_invalid_options() { let invalid_cases = vec![ From 6ec5223f50e4056b8f07a61141e7f3e67b25d5e3 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Tue, 4 Mar 2025 06:59:39 +0100 Subject: [PATCH 004/130] Extend support for INDEX parsing (#1707) Co-authored-by: Ifeanyi Ubah --- src/ast/ddl.rs | 17 ++- src/ast/dml.rs | 31 ++++- src/ast/mod.rs | 3 +- src/ast/spans.rs | 5 +- src/keywords.rs | 5 + src/parser/mod.rs | 90 +++++++++++--- tests/sqlparser_common.rs | 83 +++++++------ tests/sqlparser_postgres.rs | 230 ++++++++++++++++++++++++++++++++++++ 8 files changed, 404 insertions(+), 60 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index bb85eb06..61963143 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1174,13 +1174,20 @@ impl fmt::Display for KeyOrIndexDisplay { /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html /// [2]: https://dev.mysql.com/doc/refman/8.0/en/create-index.html /// [3]: https://www.postgresql.org/docs/14/sql-createindex.html -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum IndexType { BTree, Hash, - // TODO add Postgresql's possible indexes + GIN, + GiST, + SPGiST, + BRIN, + Bloom, + /// Users may define their own index types, which would + /// not be covered by the above variants. + Custom(Ident), } impl fmt::Display for IndexType { @@ -1188,6 +1195,12 @@ impl fmt::Display for IndexType { match self { Self::BTree => write!(f, "BTREE"), Self::Hash => write!(f, "HASH"), + Self::GIN => write!(f, "GIN"), + Self::GiST => write!(f, "GIST"), + Self::SPGiST => write!(f, "SPGIST"), + Self::BRIN => write!(f, "BRIN"), + Self::Bloom => write!(f, "BLOOM"), + Self::Custom(name) => write!(f, "{}", name), } } } diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 8cfc6741..ccea7fbc 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -34,12 +34,31 @@ pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ display_comma_separated, display_separated, query::InputFormatClause, Assignment, ClusteredBy, CommentDef, Expr, FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, - HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, - OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, Setting, SqlOption, - SqliteOnConflict, StorageSerializationPolicy, TableEngine, TableObject, TableWithJoins, Tag, - WrappedCollection, + HiveRowFormat, Ident, IndexType, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, + OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, Setting, + SqlOption, SqliteOnConflict, StorageSerializationPolicy, TableEngine, TableObject, + TableWithJoins, Tag, WrappedCollection, }; +/// Index column type. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IndexColumn { + pub column: OrderByExpr, + pub operator_class: Option, +} + +impl Display for IndexColumn { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.column)?; + if let Some(operator_class) = &self.operator_class { + write!(f, " {}", operator_class)?; + } + Ok(()) + } +} + /// CREATE INDEX statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -49,8 +68,8 @@ pub struct CreateIndex { pub name: Option, #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub table_name: ObjectName, - pub using: Option, - pub columns: Vec, + pub using: Option, + pub columns: Vec, pub unique: bool, pub concurrently: bool, pub if_not_exists: bool, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 554ec19b..e5e4aef0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -58,7 +58,7 @@ pub use self::ddl::{ ReferentialAction, TableConstraint, TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; -pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; +pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, @@ -91,6 +91,7 @@ pub use self::value::{ use crate::ast::helpers::key_value_options::KeyValueOptions; use crate::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageParamsObject}; + #[cfg(feature = "visitor")] pub use visitor::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 38e9e258..0a64fb8e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -704,7 +704,7 @@ impl Spanned for CreateIndex { let CreateIndex { name, table_name, - using, + using: _, columns, unique: _, // bool concurrently: _, // bool @@ -719,8 +719,7 @@ impl Spanned for CreateIndex { name.iter() .map(|i| i.span()) .chain(core::iter::once(table_name.span())) - .chain(using.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span())) + .chain(columns.iter().map(|i| i.column.span())) .chain(include.iter().map(|i| i.span)) .chain(with.iter().map(|i| i.span())) .chain(predicate.iter().map(|i| i.span())), diff --git a/src/keywords.rs b/src/keywords.rs index a6854f07..bda817df 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -137,11 +137,13 @@ define_keywords!( BIT, BLOB, BLOCK, + BLOOM, BLOOMFILTER, BOOL, BOOLEAN, BOTH, BOX, + BRIN, BROWSE, BTREE, BUCKET, @@ -386,6 +388,8 @@ define_keywords!( GENERATED, GEOGRAPHY, GET, + GIN, + GIST, GLOBAL, GRANT, GRANTED, @@ -805,6 +809,7 @@ define_keywords!( SPATIAL, SPECIFIC, SPECIFICTYPE, + SPGIST, SQL, SQLEXCEPTION, SQLSTATE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b11e5779..b3441538 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3955,6 +3955,18 @@ impl<'a> Parser<'a> { true } + /// If the current token is one of the given `keywords`, returns the keyword + /// that matches, without consuming the token. Otherwise, returns [`None`]. + #[must_use] + pub fn peek_one_of_keywords(&self, keywords: &[Keyword]) -> Option { + for keyword in keywords { + if self.peek_keyword(*keyword) { + return Some(*keyword); + } + } + None + } + /// If the current token is one of the given `keywords`, consume the token /// and return the keyword that matches. Otherwise, no tokens are consumed /// and returns [`None`]. @@ -6406,12 +6418,13 @@ impl<'a> Parser<'a> { }; let table_name = self.parse_object_name(false)?; let using = if self.parse_keyword(Keyword::USING) { - Some(self.parse_identifier()?) + Some(self.parse_index_type()?) } else { None }; + self.expect_token(&Token::LParen)?; - let columns = self.parse_comma_separated(Parser::parse_order_by_expr)?; + let columns = self.parse_comma_separated(Parser::parse_create_index_expr)?; self.expect_token(&Token::RParen)?; let include = if self.parse_keyword(Keyword::INCLUDE) { @@ -7629,16 +7642,30 @@ impl<'a> Parser<'a> { } pub fn parse_index_type(&mut self) -> Result { - if self.parse_keyword(Keyword::BTREE) { - Ok(IndexType::BTree) + Ok(if self.parse_keyword(Keyword::BTREE) { + IndexType::BTree } else if self.parse_keyword(Keyword::HASH) { - Ok(IndexType::Hash) + IndexType::Hash + } else if self.parse_keyword(Keyword::GIN) { + IndexType::GIN + } else if self.parse_keyword(Keyword::GIST) { + IndexType::GiST + } else if self.parse_keyword(Keyword::SPGIST) { + IndexType::SPGiST + } else if self.parse_keyword(Keyword::BRIN) { + IndexType::BRIN + } else if self.parse_keyword(Keyword::BLOOM) { + IndexType::Bloom } else { - self.expected("index type {BTREE | HASH}", self.peek_token()) - } + IndexType::Custom(self.parse_identifier()?) + }) } - /// Parse [USING {BTREE | HASH}] + /// Optionally parse the `USING` keyword, followed by an [IndexType] + /// Example: + /// ```sql + //// USING BTREE (name, age DESC) + /// ``` pub fn parse_optional_using_then_index_type( &mut self, ) -> Result, ParserError> { @@ -13631,10 +13658,42 @@ impl<'a> Parser<'a> { } } - /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) + /// Parse an [OrderByExpr] expression. pub fn parse_order_by_expr(&mut self) -> Result { + self.parse_order_by_expr_inner(false) + .map(|(order_by, _)| order_by) + } + + /// Parse an [IndexColumn]. + pub fn parse_create_index_expr(&mut self) -> Result { + self.parse_order_by_expr_inner(true) + .map(|(column, operator_class)| IndexColumn { + column, + operator_class, + }) + } + + fn parse_order_by_expr_inner( + &mut self, + with_operator_class: bool, + ) -> Result<(OrderByExpr, Option), ParserError> { let expr = self.parse_expr()?; + let operator_class: Option = if with_operator_class { + // We check that if non of the following keywords are present, then we parse an + // identifier as operator class. + if self + .peek_one_of_keywords(&[Keyword::ASC, Keyword::DESC, Keyword::NULLS, Keyword::WITH]) + .is_some() + { + None + } else { + self.maybe_parse(|parser| parser.parse_identifier())? + } + } else { + None + }; + let options = self.parse_order_by_options()?; let with_fill = if dialect_of!(self is ClickHouseDialect | GenericDialect) @@ -13645,11 +13704,14 @@ impl<'a> Parser<'a> { None }; - Ok(OrderByExpr { - expr, - options, - with_fill, - }) + Ok(( + OrderByExpr { + expr, + options, + with_fill, + }, + operator_class, + )) } fn parse_order_by_options(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2c35c243..a8ccd70a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8842,22 +8842,28 @@ fn ensure_multiple_dialects_are_tested() { #[test] fn parse_create_index() { let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test(name,age DESC)"; - let indexed_columns = vec![ - OrderByExpr { - expr: Expr::Identifier(Ident::new("name")), - options: OrderByOptions { - asc: None, - nulls_first: None, + let indexed_columns: Vec = vec![ + IndexColumn { + operator_class: None, + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("name")), + with_fill: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, }, - with_fill: None, }, - OrderByExpr { - expr: Expr::Identifier(Ident::new("age")), - options: OrderByOptions { - asc: Some(false), - nulls_first: None, + IndexColumn { + operator_class: None, + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("age")), + with_fill: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, }, - with_fill: None, }, ]; match verified_stmt(sql) { @@ -8881,23 +8887,29 @@ fn parse_create_index() { #[test] fn test_create_index_with_using_function() { - let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test USING btree (name,age DESC)"; - let indexed_columns = vec![ - OrderByExpr { - expr: Expr::Identifier(Ident::new("name")), - options: OrderByOptions { - asc: None, - nulls_first: None, + let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test USING BTREE (name,age DESC)"; + let indexed_columns: Vec = vec![ + IndexColumn { + operator_class: None, + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("name")), + with_fill: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, }, - with_fill: None, }, - OrderByExpr { - expr: Expr::Identifier(Ident::new("age")), - options: OrderByOptions { - asc: Some(false), - nulls_first: None, + IndexColumn { + operator_class: None, + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("age")), + with_fill: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, }, - with_fill: None, }, ]; match verified_stmt(sql) { @@ -8916,7 +8928,7 @@ fn test_create_index_with_using_function() { }) => { assert_eq!("idx_name", name.to_string()); assert_eq!("test", table_name.to_string()); - assert_eq!("btree", using.unwrap().to_string()); + assert_eq!("BTREE", using.unwrap().to_string()); assert_eq!(indexed_columns, columns); assert!(unique); assert!(!concurrently); @@ -8931,13 +8943,16 @@ fn test_create_index_with_using_function() { #[test] fn test_create_index_with_with_clause() { let sql = "CREATE UNIQUE INDEX title_idx ON films(title) WITH (fillfactor = 70, single_param)"; - let indexed_columns = vec![OrderByExpr { - expr: Expr::Identifier(Ident::new("title")), - options: OrderByOptions { - asc: None, - nulls_first: None, + let indexed_columns: Vec = vec![IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("title")), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, }, - with_fill: None, + operator_class: None, }]; let with_parameters = vec![ Expr::BinaryOp { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7508218f..0dfcc24e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2509,6 +2509,236 @@ fn parse_create_anonymous_index() { } } +#[test] +/// Test to verify the correctness of parsing the `CREATE INDEX` statement with optional operator classes. +/// +/// # Implementative details +/// +/// At this time, since the parser library is not intended to take care of the semantics of the SQL statements, +/// there is no way to verify the correctness of the operator classes, nor whether they are valid for the given +/// index type. This test is only intended to verify that the parser can correctly parse the statement. For this +/// reason, the test includes a `totally_not_valid` operator class. +fn parse_create_indices_with_operator_classes() { + let indices = [ + IndexType::GIN, + IndexType::GiST, + IndexType::SPGiST, + IndexType::Custom("CustomIndexType".into()), + ]; + let operator_classes: [Option; 4] = [ + None, + Some("gin_trgm_ops".into()), + Some("gist_trgm_ops".into()), + Some("totally_not_valid".into()), + ]; + + for expected_index_type in indices { + for expected_operator_class in &operator_classes { + let single_column_sql_statement = format!( + "CREATE INDEX the_index_name ON users USING {expected_index_type} (concat_users_name(first_name, last_name){})", + expected_operator_class.as_ref().map(|oc| format!(" {}", oc)) + .unwrap_or_default() + ); + let multi_column_sql_statement = format!( + "CREATE INDEX the_index_name ON users USING {expected_index_type} (column_name,concat_users_name(first_name, last_name){})", + expected_operator_class.as_ref().map(|oc| format!(" {}", oc)) + .unwrap_or_default() + ); + + let expected_function_column = IndexColumn { + column: OrderByExpr { + expr: Expr::Function(Function { + name: ObjectName(vec![ObjectNamePart::Identifier(Ident { + value: "concat_users_name".to_owned(), + quote_style: None, + span: Span::empty(), + })]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier( + Ident { + value: "first_name".to_owned(), + quote_style: None, + span: Span::empty(), + }, + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier( + Ident { + value: "last_name".to_owned(), + quote_style: None, + span: Span::empty(), + }, + ))), + ], + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: expected_operator_class.clone(), + }; + + match pg().verified_stmt(&single_column_sql_statement) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using: Some(using), + columns, + unique: false, + concurrently: false, + if_not_exists: false, + include, + nulls_distinct: None, + with, + predicate: None, + }) => { + assert_eq_vec(&["the_index_name"], &name); + assert_eq_vec(&["users"], &table_name); + assert_eq!(expected_index_type, using); + assert_eq!(expected_function_column, columns[0],); + assert!(include.is_empty()); + assert!(with.is_empty()); + } + _ => unreachable!(), + } + + match pg().verified_stmt(&multi_column_sql_statement) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using: Some(using), + columns, + unique: false, + concurrently: false, + if_not_exists: false, + include, + nulls_distinct: None, + with, + predicate: None, + }) => { + assert_eq_vec(&["the_index_name"], &name); + assert_eq_vec(&["users"], &table_name); + assert_eq!(expected_index_type, using); + assert_eq!( + IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(Ident { + value: "column_name".to_owned(), + quote_style: None, + span: Span::empty() + }), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None + }, + columns[0], + ); + assert_eq!(expected_function_column, columns[1],); + assert!(include.is_empty()); + assert!(with.is_empty()); + } + _ => unreachable!(), + } + } + } +} + +#[test] +fn parse_create_bloom() { + let sql = + "CREATE INDEX bloomidx ON tbloom USING BLOOM (i1,i2,i3) WITH (length = 80, col1 = 2, col2 = 2, col3 = 4)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using: Some(using), + columns, + unique: false, + concurrently: false, + if_not_exists: false, + include, + nulls_distinct: None, + with, + predicate: None, + }) => { + assert_eq_vec(&["bloomidx"], &name); + assert_eq_vec(&["tbloom"], &table_name); + assert_eq!(IndexType::Bloom, using); + assert_eq_vec(&["i1", "i2", "i3"], &columns); + assert!(include.is_empty()); + assert_eq!( + vec![ + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("length"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("80").into())), + }, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col1"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("2").into())), + }, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col2"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("2").into())), + }, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col3"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("4").into())), + }, + ], + with + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_brin() { + let sql = "CREATE INDEX brin_sensor_data_recorded_at ON sensor_data USING BRIN (recorded_at)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using: Some(using), + columns, + unique: false, + concurrently: false, + if_not_exists: false, + include, + nulls_distinct: None, + with, + predicate: None, + }) => { + assert_eq_vec(&["brin_sensor_data_recorded_at"], &name); + assert_eq_vec(&["sensor_data"], &table_name); + assert_eq!(IndexType::BRIN, using); + assert_eq_vec(&["recorded_at"], &columns); + assert!(include.is_empty()); + assert!(with.is_empty()); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_index_concurrently() { let sql = "CREATE INDEX CONCURRENTLY IF NOT EXISTS my_index ON my_table(col1,col2)"; From 1e54a34acdea192c3d67330e604e0bf9ce8bf866 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Tue, 11 Mar 2025 23:34:18 -0700 Subject: [PATCH 005/130] Parse MySQL `ALTER TABLE DROP FOREIGN KEY` syntax (#1762) --- src/ast/ddl.rs | 13 ++++++++++++- src/ast/spans.rs | 1 + src/parser/mod.rs | 7 ++++--- tests/sqlparser_mysql.rs | 10 ++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 61963143..99d8521c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -151,8 +151,18 @@ pub enum AlterTableOperation { }, /// `DROP PRIMARY KEY` /// - /// Note: this is a MySQL-specific operation. + /// Note: this is a [MySQL]-specific operation. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html DropPrimaryKey, + /// `DROP FOREIGN KEY ` + /// + /// Note: this is a [MySQL]-specific operation. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + DropForeignKey { + name: Ident, + }, /// `ENABLE ALWAYS RULE rewrite_rule_name` /// /// Note: this is a PostgreSQL-specific operation. @@ -530,6 +540,7 @@ impl fmt::Display for AlterTableOperation { ) } AlterTableOperation::DropPrimaryKey => write!(f, "DROP PRIMARY KEY"), + AlterTableOperation::DropForeignKey { name } => write!(f, "DROP FOREIGN KEY {name}"), AlterTableOperation::DropColumn { column_name, if_exists, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0a64fb8e..62ca9dc0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -998,6 +998,7 @@ impl Spanned for AlterTableOperation { .span() .union_opt(&with_name.as_ref().map(|n| n.span)), AlterTableOperation::DropPrimaryKey => Span::empty(), + AlterTableOperation::DropForeignKey { name } => name.span, AlterTableOperation::EnableAlwaysRule { name } => name.span, AlterTableOperation::EnableAlwaysTrigger { name } => name.span, AlterTableOperation::EnableReplicaRule { name } => name.span, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b3441538..2b95d674 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7998,10 +7998,11 @@ impl<'a> Parser<'a> { name, drop_behavior, } - } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) - && dialect_of!(self is MySqlDialect | GenericDialect) - { + } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) { AlterTableOperation::DropPrimaryKey + } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::KEY]) { + let name = self.parse_identifier()?; + AlterTableOperation::DropForeignKey { name } } else if self.parse_keyword(Keyword::PROJECTION) && dialect_of!(self is ClickHouseDialect|GenericDialect) { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8d89ce4e..560ea9da 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2273,6 +2273,16 @@ fn parse_alter_table_drop_primary_key() { ); } +#[test] +fn parse_alter_table_drop_foreign_key() { + assert_matches!( + alter_table_op( + mysql_and_generic().verified_stmt("ALTER TABLE tab DROP FOREIGN KEY foo_ibfk_1") + ), + AlterTableOperation::DropForeignKey { name } if name.value == "foo_ibfk_1" + ); +} + #[test] fn parse_alter_table_change_column() { let expected_name = ObjectName::from(vec![Ident::new("orders")]); From 3392623b00718be68268fa65298ae98f74d2eda5 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Wed, 12 Mar 2025 11:42:51 +0100 Subject: [PATCH 006/130] add support for `with` clauses (CTEs) in `delete` statements (#1764) --- src/ast/query.rs | 2 ++ src/ast/spans.rs | 1 + src/parser/mod.rs | 22 ++++++++++++++++++++++ tests/sqlparser_common.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index bed99111..12f72932 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -156,6 +156,7 @@ pub enum SetExpr { Values(Values), Insert(Statement), Update(Statement), + Delete(Statement), Table(Box), } @@ -178,6 +179,7 @@ impl fmt::Display for SetExpr { SetExpr::Values(v) => write!(f, "{v}"), SetExpr::Insert(v) => write!(f, "{v}"), SetExpr::Update(v) => write!(f, "{v}"), + SetExpr::Delete(v) => write!(f, "{v}"), SetExpr::Table(t) => write!(f, "{t}"), SetExpr::SetOperation { left, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 62ca9dc0..8c3eff3c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -191,6 +191,7 @@ impl Spanned for SetExpr { SetExpr::Insert(statement) => statement.span(), SetExpr::Table(_) => Span::empty(), SetExpr::Update(statement) => statement.span(), + SetExpr::Delete(statement) => statement.span(), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2b95d674..400a9480 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10050,6 +10050,13 @@ impl<'a> Parser<'a> { Ok(parent_type(inside_type.into())) } + /// Parse a DELETE statement, returning a `Box`ed SetExpr + /// + /// This is used to reduce the size of the stack frames in debug builds + fn parse_delete_setexpr_boxed(&mut self) -> Result, ParserError> { + Ok(Box::new(SetExpr::Delete(self.parse_delete()?))) + } + pub fn parse_delete(&mut self) -> Result { let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) { // `FROM` keyword is optional in BigQuery SQL. @@ -10249,6 +10256,21 @@ impl<'a> Parser<'a> { format_clause: None, } .into()) + } else if self.parse_keyword(Keyword::DELETE) { + Ok(Query { + with, + body: self.parse_delete_setexpr_boxed()?, + limit: None, + limit_by: vec![], + order_by: None, + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + } + .into()) } else { let body = self.parse_query_body(self.dialect.prec_unknown())?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a8ccd70a..8225d367 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7383,6 +7383,33 @@ fn parse_recursive_cte() { assert_eq!(with.cte_tables.first().unwrap(), &expected); } +#[test] +fn parse_cte_in_data_modification_statements() { + match verified_stmt("WITH x AS (SELECT 1) UPDATE t SET bar = (SELECT * FROM x)") { + Statement::Query(query) => { + assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 1)"); + assert!(matches!(*query.body, SetExpr::Update(_))); + } + other => panic!("Expected: UPDATE, got: {:?}", other), + } + + match verified_stmt("WITH t (x) AS (SELECT 9) DELETE FROM q WHERE id IN (SELECT x FROM t)") { + Statement::Query(query) => { + assert_eq!(query.with.unwrap().to_string(), "WITH t (x) AS (SELECT 9)"); + assert!(matches!(*query.body, SetExpr::Delete(_))); + } + other => panic!("Expected: DELETE, got: {:?}", other), + } + + match verified_stmt("WITH x AS (SELECT 42) INSERT INTO t SELECT foo FROM x") { + Statement::Query(query) => { + assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 42)"); + assert!(matches!(*query.body, SetExpr::Insert(_))); + } + other => panic!("Expected: INSERT, got: {:?}", other), + } +} + #[test] fn parse_derived_tables() { let sql = "SELECT a.x, b.y FROM (SELECT x FROM foo) AS a CROSS JOIN (SELECT y FROM bar) AS b"; From 85f855150fddf9326b0c2de0a5808fb46a1f2527 Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Wed, 12 Mar 2025 22:02:39 +0200 Subject: [PATCH 007/130] SET with a list of comma separated assignments (#1757) --- src/ast/mod.rs | 361 +++++++++++++++++++++--------------- src/ast/spans.rs | 15 +- src/dialect/mod.rs | 10 + src/dialect/mssql.rs | 1 + src/dialect/mysql.rs | 4 + src/keywords.rs | 2 + src/parser/mod.rs | 298 +++++++++++++++++++---------- tests/sqlparser_common.rs | 119 ++++++------ tests/sqlparser_hive.rs | 17 +- tests/sqlparser_mssql.rs | 8 +- tests/sqlparser_mysql.rs | 22 +-- tests/sqlparser_postgres.rs | 82 ++++---- 12 files changed, 564 insertions(+), 375 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e5e4aef0..8ab3fc0f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2394,6 +2394,168 @@ pub enum CreatePolicyCommand { Delete, } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Set { + /// SQL Standard-style + /// SET a = 1; + SingleAssignment { + local: bool, + hivevar: bool, + variable: ObjectName, + values: Vec, + }, + /// Snowflake-style + /// SET (a, b, ..) = (1, 2, ..); + ParenthesizedAssignments { + variables: Vec, + values: Vec, + }, + /// MySQL-style + /// SET a = 1, b = 2, ..; + MultipleAssignments { assignments: Vec }, + /// MS-SQL session + /// + /// See + SetSessionParam(SetSessionParamKind), + /// ```sql + /// SET [ SESSION | LOCAL ] ROLE role_name + /// ``` + /// + /// Sets session state. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4] + /// + /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#set-role-statement + /// [2]: https://www.postgresql.org/docs/14/sql-set-role.html + /// [3]: https://dev.mysql.com/doc/refman/8.0/en/set-role.html + /// [4]: https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10004.htm + SetRole { + /// Non-ANSI optional identifier to inform if the role is defined inside the current session (`SESSION`) or transaction (`LOCAL`). + context_modifier: ContextModifier, + /// Role name. If NONE is specified, then the current role name is removed. + role_name: Option, + }, + /// ```sql + /// SET TIME ZONE + /// ``` + /// + /// Note: this is a PostgreSQL-specific statements + /// `SET TIME ZONE ` is an alias for `SET timezone TO ` in PostgreSQL + /// However, we allow it for all dialects. + SetTimeZone { local: bool, value: Expr }, + /// ```sql + /// SET NAMES 'charset_name' [COLLATE 'collation_name'] + /// ``` + SetNames { + charset_name: Ident, + collation_name: Option, + }, + /// ```sql + /// SET NAMES DEFAULT + /// ``` + /// + /// Note: this is a MySQL-specific statement. + SetNamesDefault {}, + /// ```sql + /// SET TRANSACTION ... + /// ``` + SetTransaction { + modes: Vec, + snapshot: Option, + session: bool, + }, +} + +impl Display for Set { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ParenthesizedAssignments { variables, values } => write!( + f, + "SET ({}) = ({})", + display_comma_separated(variables), + display_comma_separated(values) + ), + Self::MultipleAssignments { assignments } => { + write!(f, "SET {}", display_comma_separated(assignments)) + } + Self::SetRole { + context_modifier, + role_name, + } => { + let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); + write!(f, "SET{context_modifier} ROLE {role_name}") + } + Self::SetSessionParam(kind) => write!(f, "SET {kind}"), + Self::SetTransaction { + modes, + snapshot, + session, + } => { + if *session { + write!(f, "SET SESSION CHARACTERISTICS AS TRANSACTION")?; + } else { + write!(f, "SET TRANSACTION")?; + } + if !modes.is_empty() { + write!(f, " {}", display_comma_separated(modes))?; + } + if let Some(snapshot_id) = snapshot { + write!(f, " SNAPSHOT {snapshot_id}")?; + } + Ok(()) + } + Self::SetTimeZone { local, value } => { + f.write_str("SET ")?; + if *local { + f.write_str("LOCAL ")?; + } + write!(f, "TIME ZONE {value}") + } + Self::SetNames { + charset_name, + collation_name, + } => { + write!(f, "SET NAMES {}", charset_name)?; + + if let Some(collation) = collation_name { + f.write_str(" COLLATE ")?; + f.write_str(collation)?; + }; + + Ok(()) + } + Self::SetNamesDefault {} => { + f.write_str("SET NAMES DEFAULT")?; + + Ok(()) + } + Set::SingleAssignment { + local, + hivevar, + variable, + values, + } => { + write!( + f, + "SET {}{}{} = {}", + if *local { "LOCAL " } else { "" }, + if *hivevar { "HIVEVAR:" } else { "" }, + variable, + display_comma_separated(values) + ) + } + } + } +} + +/// Convert a `Set` into a `Statement`. +/// Convenience function, instead of writing `Statement::Set(Set::Set...{...})` +impl From for Statement { + fn from(set: Set) -> Self { + Statement::Set(set) + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -2419,6 +2581,7 @@ pub enum Statement { compute_statistics: bool, has_table_keyword: bool, }, + Set(Set), /// ```sql /// TRUNCATE /// ``` @@ -2846,7 +3009,10 @@ pub enum Statement { /// DROP CONNECTOR /// ``` /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-DropConnector) - DropConnector { if_exists: bool, name: Ident }, + DropConnector { + if_exists: bool, + name: Ident, + }, /// ```sql /// DECLARE /// ``` @@ -2854,7 +3020,9 @@ pub enum Statement { /// /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. - Declare { stmts: Vec }, + Declare { + stmts: Vec, + }, /// ```sql /// CREATE EXTENSION [ IF NOT EXISTS ] extension_name /// [ WITH ] [ SCHEMA schema_name ] @@ -2916,67 +3084,23 @@ pub enum Statement { /// /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. - Discard { object_type: DiscardObject }, - /// ```sql - /// SET [ SESSION | LOCAL ] ROLE role_name - /// ``` - /// - /// Sets session state. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4] - /// - /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#set-role-statement - /// [2]: https://www.postgresql.org/docs/14/sql-set-role.html - /// [3]: https://dev.mysql.com/doc/refman/8.0/en/set-role.html - /// [4]: https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10004.htm - SetRole { - /// Non-ANSI optional identifier to inform if the role is defined inside the current session (`SESSION`) or transaction (`LOCAL`). - context_modifier: ContextModifier, - /// Role name. If NONE is specified, then the current role name is removed. - role_name: Option, + Discard { + object_type: DiscardObject, }, - /// ```sql - /// SET = expression; - /// SET (variable[, ...]) = (expression[, ...]); - /// ``` - /// - /// Note: this is not a standard SQL statement, but it is supported by at - /// least MySQL and PostgreSQL. Not all MySQL-specific syntactic forms are - /// supported yet. - SetVariable { - local: bool, - hivevar: bool, - variables: OneOrManyWithParens, - value: Vec, - }, - /// ```sql - /// SET TIME ZONE - /// ``` - /// - /// Note: this is a PostgreSQL-specific statements - /// `SET TIME ZONE ` is an alias for `SET timezone TO ` in PostgreSQL - SetTimeZone { local: bool, value: Expr }, - /// ```sql - /// SET NAMES 'charset_name' [COLLATE 'collation_name'] - /// ``` - SetNames { - charset_name: Ident, - collation_name: Option, - }, - /// ```sql - /// SET NAMES DEFAULT - /// ``` - /// - /// Note: this is a MySQL-specific statement. - SetNamesDefault {}, /// `SHOW FUNCTIONS` /// /// Note: this is a Presto-specific statement. - ShowFunctions { filter: Option }, + ShowFunctions { + filter: Option, + }, /// ```sql /// SHOW /// ``` /// /// Note: this is a PostgreSQL-specific statement. - ShowVariable { variable: Vec }, + ShowVariable { + variable: Vec, + }, /// ```sql /// SHOW [GLOBAL | SESSION] STATUS [LIKE 'pattern' | WHERE expr] /// ``` @@ -3060,7 +3184,9 @@ pub enum Statement { /// ``` /// /// Note: this is a MySQL-specific statement. - ShowCollation { filter: Option }, + ShowCollation { + filter: Option, + }, /// ```sql /// `USE ...` /// ``` @@ -3103,14 +3229,6 @@ pub enum Statement { has_end_keyword: bool, }, /// ```sql - /// SET TRANSACTION ... - /// ``` - SetTransaction { - modes: Vec, - snapshot: Option, - session: bool, - }, - /// ```sql /// COMMENT ON ... /// ``` /// @@ -3329,7 +3447,10 @@ pub enum Statement { /// ``` /// /// Note: this is a PostgreSQL-specific statement. - Deallocate { name: Ident, prepare: bool }, + Deallocate { + name: Ident, + prepare: bool, + }, /// ```sql /// An `EXECUTE` statement /// ``` @@ -3415,11 +3536,15 @@ pub enum Statement { /// SAVEPOINT /// ``` /// Define a new savepoint within the current transaction - Savepoint { name: Ident }, + Savepoint { + name: Ident, + }, /// ```sql /// RELEASE [ SAVEPOINT ] savepoint_name /// ``` - ReleaseSavepoint { name: Ident }, + ReleaseSavepoint { + name: Ident, + }, /// A `MERGE` statement. /// /// ```sql @@ -3499,7 +3624,9 @@ pub enum Statement { /// LOCK TABLES [READ [LOCAL] | [LOW_PRIORITY] WRITE] /// ``` /// Note: this is a MySQL-specific statement. See - LockTables { tables: Vec }, + LockTables { + tables: Vec, + }, /// ```sql /// UNLOCK TABLES /// ``` @@ -3533,14 +3660,18 @@ pub enum Statement { /// listen for a notification channel /// /// See Postgres - LISTEN { channel: Ident }, + LISTEN { + channel: Ident, + }, /// ```sql /// UNLISTEN /// ``` /// stop listening for a notification /// /// See Postgres - UNLISTEN { channel: Ident }, + UNLISTEN { + channel: Ident, + }, /// ```sql /// NOTIFY channel [ , payload ] /// ``` @@ -3580,10 +3711,6 @@ pub enum Statement { /// Snowflake `REMOVE` /// See: Remove(FileStagingCommand), - /// MS-SQL session - /// - /// See - SetSessionParam(SetSessionParamKind), /// RaiseError (MSSQL) /// RAISERROR ( { msg_id | msg_str | @local_variable } /// { , severity , state } @@ -4644,59 +4771,7 @@ impl fmt::Display for Statement { write!(f, "DISCARD {object_type}")?; Ok(()) } - Self::SetRole { - context_modifier, - role_name, - } => { - let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); - write!(f, "SET{context_modifier} ROLE {role_name}") - } - Statement::SetVariable { - local, - variables, - hivevar, - value, - } => { - f.write_str("SET ")?; - if *local { - f.write_str("LOCAL ")?; - } - let parenthesized = matches!(variables, OneOrManyWithParens::Many(_)); - write!( - f, - "{hivevar}{name} = {l_paren}{value}{r_paren}", - hivevar = if *hivevar { "HIVEVAR:" } else { "" }, - name = variables, - l_paren = parenthesized.then_some("(").unwrap_or_default(), - value = display_comma_separated(value), - r_paren = parenthesized.then_some(")").unwrap_or_default(), - ) - } - Statement::SetTimeZone { local, value } => { - f.write_str("SET ")?; - if *local { - f.write_str("LOCAL ")?; - } - write!(f, "TIME ZONE {value}") - } - Statement::SetNames { - charset_name, - collation_name, - } => { - write!(f, "SET NAMES {}", charset_name)?; - - if let Some(collation) = collation_name { - f.write_str(" COLLATE ")?; - f.write_str(collation)?; - }; - - Ok(()) - } - Statement::SetNamesDefault {} => { - f.write_str("SET NAMES DEFAULT")?; - - Ok(()) - } + Self::Set(set) => write!(f, "{set}"), Statement::ShowVariable { variable } => { write!(f, "SHOW")?; if !variable.is_empty() { @@ -4885,24 +4960,6 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::SetTransaction { - modes, - snapshot, - session, - } => { - if *session { - write!(f, "SET SESSION CHARACTERISTICS AS TRANSACTION")?; - } else { - write!(f, "SET TRANSACTION")?; - } - if !modes.is_empty() { - write!(f, " {}", display_comma_separated(modes))?; - } - if let Some(snapshot_id) = snapshot { - write!(f, " SNAPSHOT {snapshot_id}")?; - } - Ok(()) - } Statement::Commit { chain, end: end_syntax, @@ -5333,7 +5390,6 @@ impl fmt::Display for Statement { Statement::List(command) => write!(f, "LIST {command}"), Statement::Remove(command) => write!(f, "REMOVE {command}"), - Statement::SetSessionParam(kind) => write!(f, "SET {kind}"), } } } @@ -5397,6 +5453,21 @@ impl fmt::Display for SequenceOptions { } } +/// Assignment for a `SET` statement (name [=|TO] value) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetAssignment { + pub name: ObjectName, + pub value: Expr, +} + +impl fmt::Display for SetAssignment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} = {}", self.name, self.value) + } +} + /// Target of a `TRUNCATE TABLE` command /// /// Note this is its own struct because `visit_relation` requires an `ObjectName` (not a `Vec`) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8c3eff3c..fb0fc3f3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -230,11 +230,7 @@ impl Spanned for Values { /// - [Statement::Fetch] /// - [Statement::Flush] /// - [Statement::Discard] -/// - [Statement::SetRole] -/// - [Statement::SetVariable] -/// - [Statement::SetTimeZone] -/// - [Statement::SetNames] -/// - [Statement::SetNamesDefault] +/// - [Statement::Set] /// - [Statement::ShowFunctions] /// - [Statement::ShowVariable] /// - [Statement::ShowStatus] @@ -244,7 +240,6 @@ impl Spanned for Values { /// - [Statement::ShowTables] /// - [Statement::ShowCollation] /// - [Statement::StartTransaction] -/// - [Statement::SetTransaction] /// - [Statement::Comment] /// - [Statement::Commit] /// - [Statement::Rollback] @@ -445,11 +440,7 @@ impl Spanned for Statement { Statement::Fetch { .. } => Span::empty(), Statement::Flush { .. } => Span::empty(), Statement::Discard { .. } => Span::empty(), - Statement::SetRole { .. } => Span::empty(), - Statement::SetVariable { .. } => Span::empty(), - Statement::SetTimeZone { .. } => Span::empty(), - Statement::SetNames { .. } => Span::empty(), - Statement::SetNamesDefault {} => Span::empty(), + Statement::Set(_) => Span::empty(), Statement::ShowFunctions { .. } => Span::empty(), Statement::ShowVariable { .. } => Span::empty(), Statement::ShowStatus { .. } => Span::empty(), @@ -460,7 +451,6 @@ impl Spanned for Statement { Statement::ShowCollation { .. } => Span::empty(), Statement::Use(u) => u.span(), Statement::StartTransaction { .. } => Span::empty(), - Statement::SetTransaction { .. } => Span::empty(), Statement::Comment { .. } => Span::empty(), Statement::Commit { .. } => Span::empty(), Statement::Rollback { .. } => Span::empty(), @@ -509,7 +499,6 @@ impl Spanned for Statement { Statement::RenameTable { .. } => Span::empty(), Statement::RaisError { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), - Statement::SetSessionParam { .. } => Span::empty(), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index aeb097cf..8d4557e2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -399,6 +399,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports multiple `SET` statements + /// in a single statement. + /// + /// ```sql + /// SET variable = expression [, variable = expression]; + /// ``` + fn supports_comma_separated_set_assignments(&self) -> bool { + false + } + /// Returns true if the dialect supports an `EXCEPT` clause following a /// wildcard in a select list. /// diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index aeed1eb7..3db34748 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -82,6 +82,7 @@ impl Dialect for MsSqlDialect { fn supports_start_transaction_modifier(&self) -> bool { true } + fn supports_end_transaction_modifier(&self) -> bool { true } diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 0bdfc9bf..2077ea19 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -141,6 +141,10 @@ impl Dialect for MySqlDialect { fn supports_set_names(&self) -> bool { true } + + fn supports_comma_separated_set_assignments(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/keywords.rs b/src/keywords.rs index bda817df..195bbb17 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -173,6 +173,7 @@ define_keywords!( CHANNEL, CHAR, CHARACTER, + CHARACTERISTICS, CHARACTERS, CHARACTER_LENGTH, CHARSET, @@ -557,6 +558,7 @@ define_keywords!( MULTISET, MUTATION, NAME, + NAMES, NANOSECOND, NANOSECONDS, NATIONAL, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 400a9480..32a7bccd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4314,7 +4314,8 @@ impl<'a> Parser<'a> { } /// Run a parser method `f`, reverting back to the current position if unsuccessful. - /// Returns `None` if `f` returns an error + /// Returns `ParserError::RecursionLimitExceeded` if `f` returns a `RecursionLimitExceeded`. + /// Returns `Ok(None)` if `f` returns any other error. pub fn maybe_parse(&mut self, f: F) -> Result, ParserError> where F: FnMut(&mut Parser) -> Result, @@ -10978,47 +10979,108 @@ impl<'a> Parser<'a> { } else { Some(self.parse_identifier()?) }; - Ok(Statement::SetRole { + Ok(Statement::Set(Set::SetRole { context_modifier, role_name, - }) + })) } - pub fn parse_set(&mut self) -> Result { - let modifier = - self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::HIVEVAR]); - if let Some(Keyword::HIVEVAR) = modifier { - self.expect_token(&Token::Colon)?; - } else if let Some(set_role_stmt) = - self.maybe_parse(|parser| parser.parse_set_role(modifier))? - { - return Ok(set_role_stmt); + fn parse_set_values( + &mut self, + parenthesized_assignment: bool, + ) -> Result, ParserError> { + let mut values = vec![]; + + if parenthesized_assignment { + self.expect_token(&Token::LParen)?; } - let variables = if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) { - OneOrManyWithParens::One(ObjectName::from(vec!["TIMEZONE".into()])) - } else if self.dialect.supports_parenthesized_set_variables() + loop { + let value = if let Some(expr) = self.try_parse_expr_sub_query()? { + expr + } else if let Ok(expr) = self.parse_expr() { + expr + } else { + self.expected("variable value", self.peek_token())? + }; + + values.push(value); + if self.consume_token(&Token::Comma) { + continue; + } + + if parenthesized_assignment { + self.expect_token(&Token::RParen)?; + } + return Ok(values); + } + } + + fn parse_set_assignment( + &mut self, + ) -> Result<(OneOrManyWithParens, Expr), ParserError> { + let variables = if self.dialect.supports_parenthesized_set_variables() && self.consume_token(&Token::LParen) { - let variables = OneOrManyWithParens::Many( + let vars = OneOrManyWithParens::Many( self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())? .into_iter() .map(|ident| ObjectName::from(vec![ident])) .collect(), ); self.expect_token(&Token::RParen)?; - variables + vars } else { OneOrManyWithParens::One(self.parse_object_name(false)?) }; - let names = matches!(&variables, OneOrManyWithParens::One(variable) if variable.to_string().eq_ignore_ascii_case("NAMES")); + if !(self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO)) { + return self.expected("assignment operator", self.peek_token()); + } - if names && self.dialect.supports_set_names() { - if self.parse_keyword(Keyword::DEFAULT) { - return Ok(Statement::SetNamesDefault {}); + let values = self.parse_expr()?; + + Ok((variables, values)) + } + + fn parse_set(&mut self) -> Result { + let modifier = + self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::HIVEVAR]); + + if let Some(Keyword::HIVEVAR) = modifier { + self.expect_token(&Token::Colon)?; + } + + if let Some(set_role_stmt) = self.maybe_parse(|parser| parser.parse_set_role(modifier))? { + return Ok(set_role_stmt); + } + + // Handle special cases first + if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) + || self.parse_keyword(Keyword::TIMEZONE) + { + if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { + return Ok(Set::SingleAssignment { + local: modifier == Some(Keyword::LOCAL), + hivevar: modifier == Some(Keyword::HIVEVAR), + variable: ObjectName::from(vec!["TIMEZONE".into()]), + values: self.parse_set_values(false)?, + } + .into()); + } else { + // A shorthand alias for SET TIME ZONE that doesn't require + // the assignment operator. It's originally PostgreSQL specific, + // but we allow it for all the dialects + return Ok(Set::SetTimeZone { + local: modifier == Some(Keyword::LOCAL), + value: self.parse_expr()?, + } + .into()); + } + } else if self.dialect.supports_set_names() && self.parse_keyword(Keyword::NAMES) { + if self.parse_keyword(Keyword::DEFAULT) { + return Ok(Set::SetNamesDefault {}.into()); } - let charset_name = self.parse_identifier()?; let collation_name = if self.parse_one_of_keywords(&[Keyword::COLLATE]).is_some() { Some(self.parse_literal_string()?) @@ -11026,86 +11088,117 @@ impl<'a> Parser<'a> { None }; - return Ok(Statement::SetNames { + return Ok(Set::SetNames { charset_name, collation_name, - }); - } - - let parenthesized_assignment = matches!(&variables, OneOrManyWithParens::Many(_)); - - if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { - if parenthesized_assignment { - self.expect_token(&Token::LParen)?; } - - let mut values = vec![]; - loop { - let value = if let Some(expr) = self.try_parse_expr_sub_query()? { - expr - } else if let Ok(expr) = self.parse_expr() { - expr - } else { - self.expected("variable value", self.peek_token())? - }; - - values.push(value); - if self.consume_token(&Token::Comma) { - continue; - } - - if parenthesized_assignment { - self.expect_token(&Token::RParen)?; - } - return Ok(Statement::SetVariable { - local: modifier == Some(Keyword::LOCAL), - hivevar: Some(Keyword::HIVEVAR) == modifier, - variables, - value: values, - }); - } - } - - let OneOrManyWithParens::One(variable) = variables else { - return self.expected("set variable", self.peek_token()); - }; - - if variable.to_string().eq_ignore_ascii_case("TIMEZONE") { - // for some db (e.g. postgresql), SET TIME ZONE is an alias for SET TIMEZONE [TO|=] - match self.parse_expr() { - Ok(expr) => Ok(Statement::SetTimeZone { - local: modifier == Some(Keyword::LOCAL), - value: expr, - }), - _ => self.expected("timezone value", self.peek_token())?, - } - } else if variable.to_string() == "CHARACTERISTICS" { + .into()); + } else if self.parse_keyword(Keyword::CHARACTERISTICS) { self.expect_keywords(&[Keyword::AS, Keyword::TRANSACTION])?; - Ok(Statement::SetTransaction { + return Ok(Set::SetTransaction { modes: self.parse_transaction_modes()?, snapshot: None, session: true, - }) - } else if variable.to_string() == "TRANSACTION" && modifier.is_none() { + } + .into()); + } else if self.parse_keyword(Keyword::TRANSACTION) { if self.parse_keyword(Keyword::SNAPSHOT) { let snapshot_id = self.parse_value()?.value; - return Ok(Statement::SetTransaction { + return Ok(Set::SetTransaction { modes: vec![], snapshot: Some(snapshot_id), session: false, - }); + } + .into()); } - Ok(Statement::SetTransaction { + return Ok(Set::SetTransaction { modes: self.parse_transaction_modes()?, snapshot: None, session: false, - }) - } else if self.dialect.supports_set_stmt_without_operator() { - self.prev_token(); - self.parse_set_session_params() - } else { - self.expected("equals sign or TO", self.peek_token()) + } + .into()); } + + if self.dialect.supports_comma_separated_set_assignments() { + if let Some(assignments) = self + .maybe_parse(|parser| parser.parse_comma_separated(Parser::parse_set_assignment))? + { + return if assignments.len() > 1 { + let assignments = assignments + .into_iter() + .map(|(var, val)| match var { + OneOrManyWithParens::One(v) => Ok(SetAssignment { + name: v, + value: val, + }), + OneOrManyWithParens::Many(_) => { + self.expected("List of single identifiers", self.peek_token()) + } + }) + .collect::>()?; + + Ok(Set::MultipleAssignments { assignments }.into()) + } else { + let (vars, values): (Vec<_>, Vec<_>) = assignments.into_iter().unzip(); + + let variable = match vars.into_iter().next() { + Some(OneOrManyWithParens::One(v)) => Ok(v), + Some(OneOrManyWithParens::Many(_)) => self.expected( + "Single assignment or list of assignments", + self.peek_token(), + ), + None => self.expected("At least one identifier", self.peek_token()), + }?; + + Ok(Set::SingleAssignment { + local: modifier == Some(Keyword::LOCAL), + hivevar: modifier == Some(Keyword::HIVEVAR), + variable, + values, + } + .into()) + }; + } + } + + let variables = if self.dialect.supports_parenthesized_set_variables() + && self.consume_token(&Token::LParen) + { + let vars = OneOrManyWithParens::Many( + self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())? + .into_iter() + .map(|ident| ObjectName::from(vec![ident])) + .collect(), + ); + self.expect_token(&Token::RParen)?; + vars + } else { + OneOrManyWithParens::One(self.parse_object_name(false)?) + }; + + if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { + let stmt = match variables { + OneOrManyWithParens::One(var) => Set::SingleAssignment { + local: modifier == Some(Keyword::LOCAL), + hivevar: modifier == Some(Keyword::HIVEVAR), + variable: var, + values: self.parse_set_values(false)?, + }, + OneOrManyWithParens::Many(vars) => Set::ParenthesizedAssignments { + variables: vars, + values: self.parse_set_values(true)?, + }, + }; + + return Ok(stmt.into()); + } + + if self.dialect.supports_set_stmt_without_operator() { + self.prev_token(); + return self.parse_set_session_params(); + }; + + self.expected("equals sign or TO", self.peek_token()) } pub fn parse_set_session_params(&mut self) -> Result { @@ -11123,15 +11216,20 @@ impl<'a> Parser<'a> { _ => return self.expected("IO, PROFILE, TIME or XML", self.peek_token()), }; let value = self.parse_session_param_value()?; - Ok(Statement::SetSessionParam(SetSessionParamKind::Statistics( - SetSessionParamStatistics { topic, value }, - ))) + Ok( + Set::SetSessionParam(SetSessionParamKind::Statistics(SetSessionParamStatistics { + topic, + value, + })) + .into(), + ) } else if self.parse_keyword(Keyword::IDENTITY_INSERT) { let obj = self.parse_object_name(false)?; let value = self.parse_session_param_value()?; - Ok(Statement::SetSessionParam( - SetSessionParamKind::IdentityInsert(SetSessionParamIdentityInsert { obj, value }), + Ok(Set::SetSessionParam(SetSessionParamKind::IdentityInsert( + SetSessionParamIdentityInsert { obj, value }, )) + .into()) } else if self.parse_keyword(Keyword::OFFSETS) { let keywords = self.parse_comma_separated(|parser| { let next_token = parser.next_token(); @@ -11141,9 +11239,13 @@ impl<'a> Parser<'a> { } })?; let value = self.parse_session_param_value()?; - Ok(Statement::SetSessionParam(SetSessionParamKind::Offsets( - SetSessionParamOffsets { keywords, value }, - ))) + Ok( + Set::SetSessionParam(SetSessionParamKind::Offsets(SetSessionParamOffsets { + keywords, + value, + })) + .into(), + ) } else { let names = self.parse_comma_separated(|parser| { let next_token = parser.next_token(); @@ -11153,9 +11255,13 @@ impl<'a> Parser<'a> { } })?; let value = self.parse_expr()?.to_string(); - Ok(Statement::SetSessionParam(SetSessionParamKind::Generic( - SetSessionParamGeneric { names, value }, - ))) + Ok( + Set::SetSessionParam(SetSessionParamKind::Generic(SetSessionParamGeneric { + names, + value, + })) + .into(), + ) } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8225d367..c7bf287c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8555,11 +8555,11 @@ fn parse_set_transaction() { // TRANSACTION, so no need to duplicate the tests here. We just do a quick // sanity check. match verified_stmt("SET TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { - Statement::SetTransaction { + Statement::Set(Set::SetTransaction { modes, session, snapshot, - } => { + }) => { assert_eq!( modes, vec![ @@ -8578,20 +8578,17 @@ fn parse_set_transaction() { #[test] fn parse_set_variable() { match verified_stmt("SET SOMETHING = '1'") { - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local, hivevar, - variables, - value, - } => { + variable, + values, + }) => { assert!(!local); assert!(!hivevar); + assert_eq!(variable, ObjectName::from(vec!["SOMETHING".into()])); assert_eq!( - variables, - OneOrManyWithParens::One(ObjectName::from(vec!["SOMETHING".into()])) - ); - assert_eq!( - value, + values, vec![Expr::Value( (Value::SingleQuotedString("1".into())).with_empty_span() )] @@ -8603,24 +8600,17 @@ fn parse_set_variable() { let multi_variable_dialects = all_dialects_where(|d| d.supports_parenthesized_set_variables()); let sql = r#"SET (a, b, c) = (1, 2, 3)"#; match multi_variable_dialects.verified_stmt(sql) { - Statement::SetVariable { - local, - hivevar, - variables, - value, - } => { - assert!(!local); - assert!(!hivevar); + Statement::Set(Set::ParenthesizedAssignments { variables, values }) => { assert_eq!( variables, - OneOrManyWithParens::Many(vec![ + vec![ ObjectName::from(vec!["a".into()]), ObjectName::from(vec!["b".into()]), ObjectName::from(vec!["c".into()]), - ]) + ] ); assert_eq!( - value, + values, vec![ Expr::value(number("1")), Expr::value(number("2")), @@ -8680,20 +8670,17 @@ fn parse_set_variable() { #[test] fn parse_set_role_as_variable() { match verified_stmt("SET role = 'foobar'") { - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local, hivevar, - variables, - value, - } => { + variable, + values, + }) => { assert!(!local); assert!(!hivevar); + assert_eq!(variable, ObjectName::from(vec!["role".into()])); assert_eq!( - variables, - OneOrManyWithParens::One(ObjectName::from(vec!["role".into()])) - ); - assert_eq!( - value, + values, vec![Expr::Value( (Value::SingleQuotedString("foobar".into())).with_empty_span() )] @@ -8730,20 +8717,17 @@ fn parse_double_colon_cast_at_timezone() { #[test] fn parse_set_time_zone() { match verified_stmt("SET TIMEZONE = 'UTC'") { - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local, hivevar, - variables: variable, - value, - } => { + variable, + values, + }) => { assert!(!local); assert!(!hivevar); + assert_eq!(variable, ObjectName::from(vec!["TIMEZONE".into()])); assert_eq!( - variable, - OneOrManyWithParens::One(ObjectName::from(vec!["TIMEZONE".into()])) - ); - assert_eq!( - value, + values, vec![Expr::Value( (Value::SingleQuotedString("UTC".into())).with_empty_span() )] @@ -8755,20 +8739,6 @@ fn parse_set_time_zone() { one_statement_parses_to("SET TIME ZONE TO 'UTC'", "SET TIMEZONE = 'UTC'"); } -#[test] -fn parse_set_time_zone_alias() { - match verified_stmt("SET TIME ZONE 'UTC'") { - Statement::SetTimeZone { local, value } => { - assert!(!local); - assert_eq!( - value, - Expr::Value((Value::SingleQuotedString("UTC".into())).with_empty_span()) - ); - } - _ => unreachable!(), - } -} - #[test] fn parse_commit() { match verified_stmt("COMMIT") { @@ -14681,3 +14651,44 @@ fn parse_set_names() { dialects.verified_stmt("SET NAMES 'utf8'"); dialects.verified_stmt("SET NAMES UTF8 COLLATE bogus"); } + +#[test] +fn parse_multiple_set_statements() -> Result<(), ParserError> { + let dialects = all_dialects_where(|d| d.supports_comma_separated_set_assignments()); + let stmt = dialects.verified_stmt("SET @a = 1, b = 2"); + + match stmt { + Statement::Set(Set::MultipleAssignments { assignments }) => { + assert_eq!( + assignments, + vec![ + SetAssignment { + name: ObjectName::from(vec!["@a".into()]), + value: Expr::value(number("1")) + }, + SetAssignment { + name: ObjectName::from(vec!["b".into()]), + value: Expr::value(number("2")) + } + ] + ); + } + _ => panic!("Expected SetVariable with 2 variables and 2 values"), + }; + + Ok(()) +} + +#[test] +fn parse_set_time_zone_alias() { + match all_dialects().verified_stmt("SET TIME ZONE 'UTC'") { + Statement::Set(Set::SetTimeZone { local, value }) => { + assert!(!local); + assert_eq!( + value, + Expr::Value((Value::SingleQuotedString("UTC".into())).with_empty_span()) + ); + } + _ => unreachable!(), + } +} diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index d7f3c014..56fe22a0 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -22,9 +22,8 @@ use sqlparser::ast::{ ClusteredBy, CommentDef, CreateFunction, CreateFunctionBody, CreateFunctionUsing, CreateTable, - Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, - OneOrManyWithParens, OrderByExpr, OrderByOptions, SelectItem, Statement, TableFactor, - UnaryOperator, Use, Value, + Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OrderByExpr, + OrderByOptions, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::ParserError; @@ -92,7 +91,7 @@ fn parse_msck() { } #[test] -fn parse_set() { +fn parse_set_hivevar() { let set = "SET HIVEVAR:name = a, b, c_d"; hive().verified_stmt(set); } @@ -369,20 +368,20 @@ fn from_cte() { fn set_statement_with_minus() { assert_eq!( hive().verified_stmt("SET hive.tez.java.opts = -Xmx4g"), - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![ + variable: ObjectName::from(vec![ Ident::new("hive"), Ident::new("tez"), Ident::new("java"), Ident::new("opts") - ])), - value: vec![Expr::UnaryOp { + ]), + values: vec![Expr::UnaryOp { op: UnaryOperator::Minus, expr: Box::new(Expr::Identifier(Ident::new("Xmx4g"))) }], - } + }) ); assert_eq!( diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 3f313af4..386bd178 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1254,14 +1254,14 @@ fn parse_mssql_declare() { for_query: None }] }, - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("@bar")])), - value: vec![Expr::Value( + variable: ObjectName::from(vec![Ident::new("@bar")]), + values: vec![Expr::Value( (Value::Number("2".parse().unwrap(), false)).with_empty_span() )], - }, + }), Statement::Query(Box::new(Query { with: None, limit: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 560ea9da..13a8a6cc 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -617,12 +617,12 @@ fn parse_set_variables() { mysql_and_generic().verified_stmt("SET sql_mode = CONCAT(@@sql_mode, ',STRICT_TRANS_TABLES')"); assert_eq!( mysql_and_generic().verified_stmt("SET LOCAL autocommit = 1"), - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: true, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec!["autocommit".into()])), - value: vec![Expr::value(number("1"))], - } + variable: ObjectName::from(vec!["autocommit".into()]), + values: vec![Expr::value(number("1"))], + }) ); } @@ -2705,19 +2705,19 @@ fn parse_set_names() { let stmt = mysql_and_generic().verified_stmt("SET NAMES utf8mb4"); assert_eq!( stmt, - Statement::SetNames { + Statement::Set(Set::SetNames { charset_name: "utf8mb4".into(), collation_name: None, - } + }) ); let stmt = mysql_and_generic().verified_stmt("SET NAMES utf8mb4 COLLATE bogus"); assert_eq!( stmt, - Statement::SetNames { + Statement::Set(Set::SetNames { charset_name: "utf8mb4".into(), collation_name: Some("bogus".to_string()), - } + }) ); let stmt = mysql_and_generic() @@ -2725,14 +2725,14 @@ fn parse_set_names() { .unwrap(); assert_eq!( stmt, - vec![Statement::SetNames { + vec![Statement::Set(Set::SetNames { charset_name: "utf8mb4".into(), collation_name: Some("bogus".to_string()), - }] + })] ); let stmt = mysql_and_generic().verified_stmt("SET NAMES DEFAULT"); - assert_eq!(stmt, Statement::SetNamesDefault {}); + assert_eq!(stmt, Statement::Set(Set::SetNamesDefault {})); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0dfcc24e..a65c4fa3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1432,81 +1432,77 @@ fn parse_set() { let stmt = pg_and_generic().verified_stmt("SET a = b"); assert_eq!( stmt, - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), - value: vec![Expr::Identifier(Ident { + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::Identifier(Ident { value: "b".into(), quote_style: None, span: Span::empty(), })], - } + }) ); let stmt = pg_and_generic().verified_stmt("SET a = 'b'"); assert_eq!( stmt, - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), - value: vec![Expr::Value( + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::Value( (Value::SingleQuotedString("b".into())).with_empty_span() )], - } + }) ); let stmt = pg_and_generic().verified_stmt("SET a = 0"); assert_eq!( stmt, - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), - value: vec![Expr::value(number("0"))], - } + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::value(number("0"))], + }) ); let stmt = pg_and_generic().verified_stmt("SET a = DEFAULT"); assert_eq!( stmt, - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), - value: vec![Expr::Identifier(Ident::new("DEFAULT"))], - } + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::Identifier(Ident::new("DEFAULT"))], + }) ); let stmt = pg_and_generic().verified_stmt("SET LOCAL a = b"); assert_eq!( stmt, - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: true, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), - value: vec![Expr::Identifier("b".into())], - } + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::Identifier("b".into())], + }) ); let stmt = pg_and_generic().verified_stmt("SET a.b.c = b"); assert_eq!( stmt, - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![ - Ident::new("a"), - Ident::new("b"), - Ident::new("c") - ])), - value: vec![Expr::Identifier(Ident { + variable: ObjectName::from(vec![Ident::new("a"), Ident::new("b"), Ident::new("c")]), + values: vec![Expr::Identifier(Ident { value: "b".into(), quote_style: None, span: Span::empty(), })], - } + }) ); let stmt = pg_and_generic().one_statement_parses_to( @@ -1515,18 +1511,18 @@ fn parse_set() { ); assert_eq!( stmt, - Statement::SetVariable { + Statement::Set(Set::SingleAssignment { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName::from(vec![ + variable: ObjectName::from(vec![ Ident::new("hive"), Ident::new("tez"), Ident::new("auto"), Ident::new("reducer"), Ident::new("parallelism") - ])), - value: vec![Expr::Value((Value::Boolean(false)).with_empty_span())], - } + ]), + values: vec![Expr::Value((Value::Boolean(false)).with_empty_span())], + }) ); pg_and_generic().one_statement_parses_to("SET a TO b", "SET a = b"); @@ -1560,10 +1556,10 @@ fn parse_set_role() { let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, - Statement::SetRole { + Statement::Set(Set::SetRole { context_modifier: ContextModifier::Session, role_name: None, - } + }) ); assert_eq!(query, stmt.to_string()); @@ -1571,14 +1567,14 @@ fn parse_set_role() { let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, - Statement::SetRole { + Statement::Set(Set::SetRole { context_modifier: ContextModifier::Local, role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\"'), span: Span::empty(), }), - } + }) ); assert_eq!(query, stmt.to_string()); @@ -1586,14 +1582,14 @@ fn parse_set_role() { let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, - Statement::SetRole { + Statement::Set(Set::SetRole { context_modifier: ContextModifier::None, role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\''), span: Span::empty(), }), - } + }) ); assert_eq!(query, stmt.to_string()); } @@ -2982,16 +2978,16 @@ fn test_transaction_statement() { let statement = pg().verified_stmt("SET TRANSACTION SNAPSHOT '000003A1-1'"); assert_eq!( statement, - Statement::SetTransaction { + Statement::Set(Set::SetTransaction { modes: vec![], snapshot: Some(Value::SingleQuotedString(String::from("000003A1-1"))), session: false - } + }) ); let statement = pg().verified_stmt("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE"); assert_eq!( statement, - Statement::SetTransaction { + Statement::Set(Set::SetTransaction { modes: vec![ TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), TransactionMode::AccessMode(TransactionAccessMode::ReadWrite), @@ -2999,7 +2995,7 @@ fn test_transaction_statement() { ], snapshot: None, session: true - } + }) ); } From fb578bb419d08d2d5b49fb75a61f1ddd6df77ba4 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Wed, 12 Mar 2025 13:24:06 -0700 Subject: [PATCH 008/130] Preserve MySQL-style `LIMIT , ` syntax (#1765) --- src/ast/mod.rs | 23 ++-- src/ast/query.rs | 73 ++++++++--- src/ast/spans.rs | 31 +++-- src/ast/visitor.rs | 4 +- src/parser/mod.rs | 109 ++++++++------- tests/sqlparser_clickhouse.rs | 15 ++- tests/sqlparser_common.rs | 240 +++++++++++++++++++--------------- tests/sqlparser_mssql.rs | 12 +- tests/sqlparser_mysql.rs | 66 +++------- tests/sqlparser_postgres.rs | 20 +-- 10 files changed, 327 insertions(+), 266 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8ab3fc0f..139850e8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -66,17 +66,18 @@ pub use self::query::{ FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, - LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, - NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, - OrderBy, OrderByExpr, OrderByKind, OrderByOptions, PivotValueSource, ProjectionSelect, Query, - RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, - Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, - SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, - TableFactor, TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, TableIndexHints, - TableIndexType, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod, - TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, - TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, UpdateTableFromKind, - ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, + LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, + Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, + OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, PivotValueSource, + ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, + ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, + SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Setting, + SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, + TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample, + TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, + TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, + TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, + WildcardAdditionalOptions, With, WithFill, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index 12f72932..1b30dcf1 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -43,14 +43,8 @@ pub struct Query { pub body: Box, /// ORDER BY pub order_by: Option, - /// `LIMIT { | ALL }` - pub limit: Option, - - /// `LIMIT { } BY { ,,... } }` - pub limit_by: Vec, - - /// `OFFSET [ { ROW | ROWS } ]` - pub offset: Option, + /// `LIMIT ... OFFSET ... | LIMIT , ` + pub limit_clause: Option, /// `FETCH { FIRST | NEXT } [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES }` pub fetch: Option, /// `FOR { UPDATE | SHARE } [ OF table_name ] [ SKIP LOCKED | NOWAIT ]` @@ -79,14 +73,9 @@ impl fmt::Display for Query { if let Some(ref order_by) = self.order_by { write!(f, " {order_by}")?; } - if let Some(ref limit) = self.limit { - write!(f, " LIMIT {limit}")?; - } - if let Some(ref offset) = self.offset { - write!(f, " {offset}")?; - } - if !self.limit_by.is_empty() { - write!(f, " BY {}", display_separated(&self.limit_by, ", "))?; + + if let Some(ref limit_clause) = self.limit_clause { + limit_clause.fmt(f)?; } if let Some(ref settings) = self.settings { write!(f, " SETTINGS {}", display_comma_separated(settings))?; @@ -2374,6 +2363,58 @@ impl fmt::Display for OrderByOptions { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum LimitClause { + /// Standard SQL syntax + /// + /// `LIMIT [BY ,,...] [OFFSET ]` + LimitOffset { + /// `LIMIT { | ALL }` + limit: Option, + /// `OFFSET [ { ROW | ROWS } ]` + offset: Option, + /// `BY { ,,... } }` + /// + /// [ClickHouse](https://clickhouse.com/docs/sql-reference/statements/select/limit-by) + limit_by: Vec, + }, + /// [MySQL]-specific syntax; the order of expressions is reversed. + /// + /// `LIMIT , ` + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/select.html + OffsetCommaLimit { offset: Expr, limit: Expr }, +} + +impl fmt::Display for LimitClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + LimitClause::LimitOffset { + limit, + limit_by, + offset, + } => { + if let Some(ref limit) = limit { + write!(f, " LIMIT {limit}")?; + } + if let Some(ref offset) = offset { + write!(f, " {offset}")?; + } + if !limit_by.is_empty() { + debug_assert!(limit.is_some()); + write!(f, " BY {}", display_separated(limit_by, ", "))?; + } + Ok(()) + } + LimitClause::OffsetCommaLimit { offset, limit } => { + write!(f, " LIMIT {}, {}", offset, limit) + } + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index fb0fc3f3..a4f5eb46 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -29,8 +29,8 @@ use super::{ Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, - OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, + LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, + Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, @@ -94,9 +94,7 @@ impl Spanned for Query { with, body, order_by, - limit, - limit_by, - offset, + limit_clause, fetch, locks: _, // todo for_clause: _, // todo, mssql specific @@ -109,14 +107,31 @@ impl Spanned for Query { .map(|i| i.span()) .chain(core::iter::once(body.span())) .chain(order_by.as_ref().map(|i| i.span())) - .chain(limit.as_ref().map(|i| i.span())) - .chain(limit_by.iter().map(|i| i.span())) - .chain(offset.as_ref().map(|i| i.span())) + .chain(limit_clause.as_ref().map(|i| i.span())) .chain(fetch.as_ref().map(|i| i.span())), ) } } +impl Spanned for LimitClause { + fn span(&self) -> Span { + match self { + LimitClause::LimitOffset { + limit, + offset, + limit_by, + } => union_spans( + limit + .iter() + .map(|i| i.span()) + .chain(offset.as_ref().map(|i| i.span())) + .chain(limit_by.iter().map(|i| i.span())), + ), + LimitClause::OffsetCommaLimit { offset, limit } => offset.span().union(&limit.span()), + } + } +} + impl Spanned for Offset { fn span(&self) -> Span { let Offset { diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index a5d355fe..50985a3e 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -523,7 +523,7 @@ where /// // Remove all select limits in sub-queries /// visit_expressions_mut(&mut statements, |expr| { /// if let Expr::Subquery(q) = expr { -/// q.limit = None +/// q.limit_clause = None; /// } /// ControlFlow::<()>::Continue(()) /// }); @@ -647,7 +647,7 @@ where /// // Remove all select limits in outer statements (not in sub-queries) /// visit_statements_mut(&mut statements, |stmt| { /// if let Statement::Query(q) = stmt { -/// q.limit = None +/// q.limit_clause = None; /// } /// ControlFlow::<()>::Continue(()) /// }); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 32a7bccd..d3c48a6e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9491,6 +9491,60 @@ impl<'a> Parser<'a> { } } + fn parse_optional_limit_clause(&mut self) -> Result, ParserError> { + let mut offset = if self.parse_keyword(Keyword::OFFSET) { + Some(self.parse_offset()?) + } else { + None + }; + + let (limit, limit_by) = if self.parse_keyword(Keyword::LIMIT) { + let expr = self.parse_limit()?; + + if self.dialect.supports_limit_comma() + && offset.is_none() + && expr.is_some() // ALL not supported with comma + && self.consume_token(&Token::Comma) + { + let offset = expr.ok_or_else(|| { + ParserError::ParserError( + "Missing offset for LIMIT , ".to_string(), + ) + })?; + return Ok(Some(LimitClause::OffsetCommaLimit { + offset, + limit: self.parse_expr()?, + })); + } + + let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keyword(Keyword::BY) + { + Some(self.parse_comma_separated(Parser::parse_expr)?) + } else { + None + }; + + (Some(expr), limit_by) + } else { + (None, None) + }; + + if offset.is_none() && limit.is_some() && self.parse_keyword(Keyword::OFFSET) { + offset = Some(self.parse_offset()?); + } + + if offset.is_some() || (limit.is_some() && limit != Some(None)) || limit_by.is_some() { + Ok(Some(LimitClause::LimitOffset { + limit: limit.unwrap_or_default(), + offset, + limit_by: limit_by.unwrap_or_default(), + })) + } else { + Ok(None) + } + } + /// Parse a table object for insertion /// e.g. `some_database.some_table` or `FUNCTION some_table_func(...)` pub fn parse_table_object(&mut self) -> Result { @@ -10231,10 +10285,8 @@ impl<'a> Parser<'a> { Ok(Query { with, body: self.parse_insert_setexpr_boxed()?, - limit: None, - limit_by: vec![], order_by: None, - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -10246,10 +10298,8 @@ impl<'a> Parser<'a> { Ok(Query { with, body: self.parse_update_setexpr_boxed()?, - limit: None, - limit_by: vec![], order_by: None, - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -10261,10 +10311,8 @@ impl<'a> Parser<'a> { Ok(Query { with, body: self.parse_delete_setexpr_boxed()?, - limit: None, - limit_by: vec![], + limit_clause: None, order_by: None, - offset: None, fetch: None, locks: vec![], for_clause: None, @@ -10277,40 +10325,7 @@ impl<'a> Parser<'a> { let order_by = self.parse_optional_order_by()?; - let mut limit = None; - let mut offset = None; - - for _x in 0..2 { - if limit.is_none() && self.parse_keyword(Keyword::LIMIT) { - limit = self.parse_limit()? - } - - if offset.is_none() && self.parse_keyword(Keyword::OFFSET) { - offset = Some(self.parse_offset()?) - } - - if self.dialect.supports_limit_comma() - && limit.is_some() - && offset.is_none() - && self.consume_token(&Token::Comma) - { - // MySQL style LIMIT x,y => LIMIT y OFFSET x. - // Check for more details. - offset = Some(Offset { - value: limit.unwrap(), - rows: OffsetRows::None, - }); - limit = Some(self.parse_expr()?); - } - } - - let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect) - && self.parse_keyword(Keyword::BY) - { - self.parse_comma_separated(Parser::parse_expr)? - } else { - vec![] - }; + let limit_clause = self.parse_optional_limit_clause()?; let settings = self.parse_settings()?; @@ -10347,9 +10362,7 @@ impl<'a> Parser<'a> { with, body, order_by, - limit, - limit_by, - offset, + limit_clause, fetch, locks, for_clause, @@ -11809,9 +11822,7 @@ impl<'a> Parser<'a> { with: None, body: Box::new(values), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 72a64a48..c56f9886 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -944,6 +944,12 @@ fn parse_limit_by() { clickhouse_and_generic().verified_stmt( r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC LIMIT 1 BY asset, toStartOfDay(created_at)"#, ); + clickhouse_and_generic().parse_sql_statements( + r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC BY asset, toStartOfDay(created_at)"#, + ).expect_err("BY without LIMIT"); + clickhouse_and_generic() + .parse_sql_statements("SELECT * FROM T OFFSET 5 BY foo") + .expect_err("BY with OFFSET but without LIMIT"); } #[test] @@ -1107,7 +1113,14 @@ fn parse_select_order_by_with_fill_interpolate() { }, select.order_by.expect("ORDER BY expected") ); - assert_eq!(Some(Expr::value(number("2"))), select.limit); + assert_eq!( + select.limit_clause, + Some(LimitClause::LimitOffset { + limit: Some(Expr::value(number("2"))), + offset: None, + limit_by: vec![] + }) + ); } #[test] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c7bf287c..b5d42ea6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -483,9 +483,7 @@ fn parse_update_set_from() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -900,7 +898,12 @@ fn parse_simple_select() { assert!(select.distinct.is_none()); assert_eq!(3, select.projection.len()); let select = verified_query(sql); - assert_eq!(Some(Expr::value(number("5"))), select.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("5"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), select.limit_clause); } #[test] @@ -908,14 +911,31 @@ fn parse_limit() { verified_stmt("SELECT * FROM user LIMIT 1"); } +#[test] +fn parse_invalid_limit_by() { + all_dialects() + .parse_sql_statements("SELECT * FROM user BY name") + .expect_err("BY without LIMIT"); +} + #[test] fn parse_limit_is_not_an_alias() { // In dialects supporting LIMIT it shouldn't be parsed as a table alias let ast = verified_query("SELECT id FROM customer LIMIT 1"); - assert_eq!(Some(Expr::value(number("1"))), ast.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("1"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), ast.limit_clause); let ast = verified_query("SELECT 1 LIMIT 5"); - assert_eq!(Some(Expr::value(number("5"))), ast.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("5"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), ast.limit_clause); } #[test] @@ -2493,7 +2513,12 @@ fn parse_select_order_by_limit() { ]), select.order_by.expect("ORDER BY expected").kind ); - assert_eq!(Some(Expr::value(number("2"))), select.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("2"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), select.limit_clause); } #[test] @@ -2654,7 +2679,12 @@ fn parse_select_order_by_nulls_order() { ]), select.order_by.expect("ORDER BY expeccted").kind ); - assert_eq!(Some(Expr::value(number("2"))), select.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("2"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), select.limit_clause); } #[test] @@ -2864,6 +2894,14 @@ fn parse_limit_accepts_all() { "SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT ALL", "SELECT id, fname, lname FROM customer WHERE id = 1", ); + one_statement_parses_to( + "SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT ALL OFFSET 1", + "SELECT id, fname, lname FROM customer WHERE id = 1 OFFSET 1", + ); + one_statement_parses_to( + "SELECT id, fname, lname FROM customer WHERE id = 1 OFFSET 1 LIMIT ALL", + "SELECT id, fname, lname FROM customer WHERE id = 1 OFFSET 1", + ); } #[test] @@ -4247,9 +4285,7 @@ fn parse_create_table_as_table() { schema_name: None, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -4274,9 +4310,7 @@ fn parse_create_table_as_table() { schema_name: Some("schema_name".to_string()), }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -6273,9 +6307,7 @@ fn parse_interval_and_or_xor() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -8175,55 +8207,65 @@ fn parse_offset() { let dialects = all_dialects_where(|d| !d.is_column_alias(&Keyword::OFFSET, &mut Parser::new(d))); - let expect = Some(Offset { - value: Expr::value(number("2")), - rows: OffsetRows::Rows, + let expected_limit_clause = &Some(LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { + value: Expr::value(number("2")), + rows: OffsetRows::Rows, + }), + limit_by: vec![], }); let ast = dialects.verified_query("SELECT foo FROM bar OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); + assert_eq!(&ast.limit_clause, expected_limit_clause); let ast = dialects.verified_query("SELECT foo FROM bar WHERE foo = 4 OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); + assert_eq!(&ast.limit_clause, expected_limit_clause); let ast = dialects.verified_query("SELECT foo FROM bar ORDER BY baz OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); + assert_eq!(&ast.limit_clause, expected_limit_clause); let ast = dialects.verified_query("SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); + assert_eq!(&ast.limit_clause, expected_limit_clause); let ast = dialects.verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS) OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); + assert_eq!(&ast.limit_clause, expected_limit_clause); match *ast.body { SetExpr::Select(s) => match only(s.from).relation { TableFactor::Derived { subquery, .. } => { - assert_eq!(subquery.offset, expect); + assert_eq!(&subquery.limit_clause, expected_limit_clause); } _ => panic!("Test broke"), }, _ => panic!("Test broke"), } - let ast = dialects.verified_query("SELECT 'foo' OFFSET 0 ROWS"); - assert_eq!( - ast.offset, - Some(Offset { + let expected_limit_clause = LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { value: Expr::value(number("0")), rows: OffsetRows::Rows, - }) - ); - let ast = dialects.verified_query("SELECT 'foo' OFFSET 1 ROW"); - assert_eq!( - ast.offset, - Some(Offset { + }), + limit_by: vec![], + }; + let ast = dialects.verified_query("SELECT 'foo' OFFSET 0 ROWS"); + assert_eq!(ast.limit_clause, Some(expected_limit_clause)); + let expected_limit_clause = LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { value: Expr::value(number("1")), rows: OffsetRows::Row, - }) - ); - let ast = dialects.verified_query("SELECT 'foo' OFFSET 1"); - assert_eq!( - ast.offset, - Some(Offset { - value: Expr::value(number("1")), + }), + limit_by: vec![], + }; + let ast = dialects.verified_query("SELECT 'foo' OFFSET 1 ROW"); + assert_eq!(ast.limit_clause, Some(expected_limit_clause)); + let expected_limit_clause = LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { + value: Expr::value(number("2")), rows: OffsetRows::None, - }) - ); + }), + limit_by: vec![], + }; + let ast = dialects.verified_query("SELECT 'foo' OFFSET 2"); + assert_eq!(ast.limit_clause, Some(expected_limit_clause)); } #[test] @@ -8273,13 +8315,15 @@ fn parse_fetch() { let ast = verified_query( "SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY", ); - assert_eq!( - ast.offset, - Some(Offset { + let expected_limit_clause = Some(LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { value: Expr::value(number("2")), rows: OffsetRows::Rows, - }) - ); + }), + limit_by: vec![], + }); + assert_eq!(ast.limit_clause, expected_limit_clause); assert_eq!(ast.fetch, fetch_first_two_rows_only); let ast = verified_query( "SELECT foo FROM (SELECT * FROM bar FETCH FIRST 2 ROWS ONLY) FETCH FIRST 2 ROWS ONLY", @@ -8295,24 +8339,20 @@ fn parse_fetch() { _ => panic!("Test broke"), } let ast = verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY) OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY"); - assert_eq!( - ast.offset, - Some(Offset { + let expected_limit_clause = &Some(LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { value: Expr::value(number("2")), rows: OffsetRows::Rows, - }) - ); + }), + limit_by: vec![], + }); + assert_eq!(&ast.limit_clause, expected_limit_clause); assert_eq!(ast.fetch, fetch_first_two_rows_only); match *ast.body { SetExpr::Select(s) => match only(s.from).relation { TableFactor::Derived { subquery, .. } => { - assert_eq!( - subquery.offset, - Some(Offset { - value: Expr::value(number("2")), - rows: OffsetRows::Rows, - }) - ); + assert_eq!(&subquery.limit_clause, expected_limit_clause); assert_eq!(subquery.fetch, fetch_first_two_rows_only); } _ => panic!("Test broke"), @@ -9358,9 +9398,7 @@ fn parse_merge() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -9678,21 +9716,18 @@ fn test_placeholder() { }) ); - let sql = "SELECT * FROM student LIMIT $1 OFFSET $2"; - let ast = dialects.verified_query(sql); - assert_eq!( - ast.limit, - Some(Expr::Value( - (Value::Placeholder("$1".into())).with_empty_span() - )) - ); - assert_eq!( - ast.offset, - Some(Offset { + let ast = dialects.verified_query("SELECT * FROM student LIMIT $1 OFFSET $2"); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::Value( + (Value::Placeholder("$1".into())).with_empty_span(), + )), + offset: Some(Offset { value: Expr::Value((Value::Placeholder("$2".into())).with_empty_span()), rows: OffsetRows::None, }), - ); + limit_by: vec![], + }; + assert_eq!(ast.limit_clause, Some(expected_limit_clause)); let dialects = TestedDialects::new(vec![ Box::new(GenericDialect {}), @@ -9772,40 +9807,34 @@ fn verified_expr(query: &str) -> Expr { #[test] fn parse_offset_and_limit() { let sql = "SELECT foo FROM bar LIMIT 1 OFFSET 2"; - let expect = Some(Offset { - value: Expr::value(number("2")), - rows: OffsetRows::None, + let expected_limit_clause = Some(LimitClause::LimitOffset { + limit: Some(Expr::value(number("1"))), + offset: Some(Offset { + value: Expr::value(number("2")), + rows: OffsetRows::None, + }), + limit_by: vec![], }); let ast = verified_query(sql); - assert_eq!(ast.offset, expect); - assert_eq!(ast.limit, Some(Expr::value(number("1")))); + assert_eq!(ast.limit_clause, expected_limit_clause); // different order is OK one_statement_parses_to("SELECT foo FROM bar OFFSET 2 LIMIT 1", sql); // mysql syntax is ok for some dialects - TestedDialects::new(vec![ - Box::new(GenericDialect {}), - Box::new(MySqlDialect {}), - Box::new(SQLiteDialect {}), - Box::new(ClickHouseDialect {}), - ]) - .one_statement_parses_to("SELECT foo FROM bar LIMIT 2, 1", sql); + all_dialects_where(|d| d.supports_limit_comma()) + .verified_query("SELECT foo FROM bar LIMIT 2, 1"); // expressions are allowed let sql = "SELECT foo FROM bar LIMIT 1 + 2 OFFSET 3 * 4"; let ast = verified_query(sql); - assert_eq!( - ast.limit, - Some(Expr::BinaryOp { + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::BinaryOp { left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Plus, right: Box::new(Expr::value(number("2"))), }), - ); - assert_eq!( - ast.offset, - Some(Offset { + offset: Some(Offset { value: Expr::BinaryOp { left: Box::new(Expr::value(number("3"))), op: BinaryOperator::Multiply, @@ -9813,7 +9842,12 @@ fn parse_offset_and_limit() { }, rows: OffsetRows::None, }), - ); + limit_by: vec![], + }; + assert_eq!(ast.limit_clause, Some(expected_limit_clause),); + + // OFFSET without LIMIT + verified_stmt("SELECT foo FROM bar OFFSET 2"); // Can't repeat OFFSET / LIMIT let res = parse_sql_statements("SELECT foo FROM bar OFFSET 2 OFFSET 2"); @@ -11227,9 +11261,7 @@ fn parse_unload() { flavor: SelectFlavor::Standard, }))), with: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -12400,9 +12432,7 @@ fn test_extract_seconds_ok() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -14265,11 +14295,9 @@ fn test_select_from_first() { flavor, }))), order_by: None, - limit: None, - offset: None, + limit_clause: None, fetch: None, locks: vec![], - limit_by: vec![], for_clause: None, settings: None, format_clause: None, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 386bd178..af71d252 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -107,9 +107,7 @@ fn parse_create_procedure() { or_alter: true, body: vec![Statement::Query(Box::new(Query { with: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1161,9 +1159,7 @@ fn parse_substring_in_select() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1264,9 +1260,7 @@ fn parse_mssql_declare() { }), Statement::Query(Box::new(Query { with: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 13a8a6cc..a5633593 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1107,9 +1107,7 @@ fn parse_escaped_quote_identifiers_with_escape() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1161,9 +1159,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1209,9 +1205,7 @@ fn parse_escaped_backticks_with_escape() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1261,9 +1255,7 @@ fn parse_escaped_backticks_with_no_escape() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1438,9 +1430,7 @@ fn parse_simple_insert() { ] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1488,9 +1478,7 @@ fn parse_ignore_insert() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1538,9 +1526,7 @@ fn parse_priority_insert() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1585,9 +1571,7 @@ fn parse_priority_insert() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1634,9 +1618,7 @@ fn parse_insert_as() { )]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1698,9 +1680,7 @@ fn parse_insert_as() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1749,9 +1729,7 @@ fn parse_replace_insert() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1791,9 +1769,7 @@ fn parse_empty_row_insert() { rows: vec![vec![], vec![]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -1857,9 +1833,7 @@ fn parse_insert_with_on_duplicate_update() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -2596,9 +2570,7 @@ fn parse_substring_in_select() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -2737,10 +2709,8 @@ fn parse_set_names() { #[test] fn parse_limit_my_sql_syntax() { - mysql_and_generic().one_statement_parses_to( - "SELECT id, fname, lname FROM customer LIMIT 5, 10", - "SELECT id, fname, lname FROM customer LIMIT 10 OFFSET 5", - ); + mysql_and_generic().verified_stmt("SELECT id, fname, lname FROM customer LIMIT 10 OFFSET 5"); + mysql_and_generic().verified_stmt("SELECT id, fname, lname FROM customer LIMIT 5, 10"); mysql_and_generic().verified_stmt("SELECT * FROM user LIMIT ? OFFSET ?"); } @@ -2903,9 +2873,7 @@ fn parse_hex_string_introducer() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a65c4fa3..1a98870f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1319,9 +1319,7 @@ fn parse_copy_to() { flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -2955,9 +2953,7 @@ fn parse_array_subquery_expr() { }))), }), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -4747,9 +4743,7 @@ fn test_simple_postgres_insert_with_alias() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -4820,9 +4814,7 @@ fn test_simple_postgres_insert_with_alias() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, @@ -4891,9 +4883,7 @@ fn test_simple_insert_with_quoted_alias() { ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, From cf4ab7f9ab031d168e10f8dbaaa3fa07f15acc63 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Thu, 13 Mar 2025 20:51:29 +0100 Subject: [PATCH 009/130] Add support for `DROP MATERIALIZED VIEW` (#1743) --- src/ast/mod.rs | 2 ++ src/parser/mod.rs | 4 +++- tests/sqlparser_common.rs | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 139850e8..4c0ffea9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6733,6 +6733,7 @@ impl fmt::Display for HavingBoundKind { pub enum ObjectType { Table, View, + MaterializedView, Index, Schema, Database, @@ -6747,6 +6748,7 @@ impl fmt::Display for ObjectType { f.write_str(match self { ObjectType::Table => "TABLE", ObjectType::View => "VIEW", + ObjectType::MaterializedView => "MATERIALIZED VIEW", ObjectType::Index => "INDEX", ObjectType::Schema => "SCHEMA", ObjectType::Database => "DATABASE", diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d3c48a6e..60e1c146 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5806,6 +5806,8 @@ impl<'a> Parser<'a> { ObjectType::Table } else if self.parse_keyword(Keyword::VIEW) { ObjectType::View + } else if self.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEW]) { + ObjectType::MaterializedView } else if self.parse_keyword(Keyword::INDEX) { ObjectType::Index } else if self.parse_keyword(Keyword::ROLE) { @@ -5836,7 +5838,7 @@ impl<'a> Parser<'a> { return self.parse_drop_extension(); } else { return self.expected( - "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, or VIEW after DROP", + "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, or MATERIALIZED VIEW after DROP", self.peek_token(), ); }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b5d42ea6..d0edafb7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8189,6 +8189,9 @@ fn parse_drop_view() { } _ => unreachable!(), } + + verified_stmt("DROP MATERIALIZED VIEW a.b.c"); + verified_stmt("DROP MATERIALIZED VIEW IF EXISTS a.b.c"); } #[test] From 862e887a66cf8ede2dc3a641db7cdcf52b061b76 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Fri, 14 Mar 2025 07:49:25 +0100 Subject: [PATCH 010/130] Add `CASE` and `IF` statement support (#1741) --- src/ast/mod.rs | 198 ++++++++++++++++++++++++++++++++++++-- src/ast/spans.rs | 78 ++++++++++++--- src/keywords.rs | 1 + src/parser/mod.rs | 104 ++++++++++++++++++++ tests/sqlparser_common.rs | 114 ++++++++++++++++++++++ 5 files changed, 473 insertions(+), 22 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4c0ffea9..66fd4c6f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -151,6 +151,15 @@ where DisplaySeparated { slice, sep: ", " } } +/// Writes the given statements to the formatter, each ending with +/// a semicolon and space separated. +fn format_statement_list(f: &mut fmt::Formatter, statements: &[Statement]) -> fmt::Result { + write!(f, "{}", display_separated(statements, "; "))?; + // We manually insert semicolon for the last statement, + // since display_separated doesn't handle that case. + write!(f, ";") +} + /// An identifier, decomposed into its value or character data and the quote style. #[derive(Debug, Clone, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -2080,6 +2089,173 @@ pub enum Password { NullPassword, } +/// A `CASE` statement. +/// +/// Examples: +/// ```sql +/// CASE +/// WHEN EXISTS(SELECT 1) +/// THEN SELECT 1 FROM T; +/// WHEN EXISTS(SELECT 2) +/// THEN SELECT 1 FROM U; +/// ELSE +/// SELECT 1 FROM V; +/// END CASE; +/// ``` +/// +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#case_search_expression) +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/case) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CaseStatement { + pub match_expr: Option, + pub when_blocks: Vec, + pub else_block: Option>, + /// TRUE if the statement ends with `END CASE` (vs `END`). + pub has_end_case: bool, +} + +impl fmt::Display for CaseStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let CaseStatement { + match_expr, + when_blocks, + else_block, + has_end_case, + } = self; + + write!(f, "CASE")?; + + if let Some(expr) = match_expr { + write!(f, " {expr}")?; + } + + if !when_blocks.is_empty() { + write!(f, " {}", display_separated(when_blocks, " "))?; + } + + if let Some(else_block) = else_block { + write!(f, " ELSE ")?; + format_statement_list(f, else_block)?; + } + + write!(f, " END")?; + if *has_end_case { + write!(f, " CASE")?; + } + + Ok(()) + } +} + +/// An `IF` statement. +/// +/// Examples: +/// ```sql +/// IF TRUE THEN +/// SELECT 1; +/// SELECT 2; +/// ELSEIF TRUE THEN +/// SELECT 3; +/// ELSE +/// SELECT 4; +/// END IF +/// ``` +/// +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#if) +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/if) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IfStatement { + pub if_block: ConditionalStatements, + pub elseif_blocks: Vec, + pub else_block: Option>, +} + +impl fmt::Display for IfStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let IfStatement { + if_block, + elseif_blocks, + else_block, + } = self; + + write!(f, "{if_block}")?; + + if !elseif_blocks.is_empty() { + write!(f, " {}", display_separated(elseif_blocks, " "))?; + } + + if let Some(else_block) = else_block { + write!(f, " ELSE ")?; + format_statement_list(f, else_block)?; + } + + write!(f, " END IF")?; + + Ok(()) + } +} + +/// Represents a type of [ConditionalStatements] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ConditionalStatementKind { + /// `WHEN THEN ` + When, + /// `IF THEN ` + If, + /// `ELSEIF THEN ` + ElseIf, +} + +/// A block within a [Statement::Case] or [Statement::If]-like statement +/// +/// Examples: +/// ```sql +/// WHEN EXISTS(SELECT 1) THEN SELECT 1; +/// +/// IF TRUE THEN SELECT 1; SELECT 2; +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ConditionalStatements { + /// The condition expression. + pub condition: Expr, + /// Statement list of the `THEN` clause. + pub statements: Vec, + pub kind: ConditionalStatementKind, +} + +impl fmt::Display for ConditionalStatements { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let ConditionalStatements { + condition: expr, + statements, + kind, + } = self; + + let kind = match kind { + ConditionalStatementKind::When => "WHEN", + ConditionalStatementKind::If => "IF", + ConditionalStatementKind::ElseIf => "ELSEIF", + }; + + write!(f, "{kind} {expr} THEN")?; + + if !statements.is_empty() { + write!(f, " ")?; + format_statement_list(f, statements)?; + } + + Ok(()) + } +} + /// Represents an expression assignment within a variable `DECLARE` statement. /// /// Examples: @@ -2647,6 +2823,10 @@ pub enum Statement { file_format: Option, source: Box, }, + /// A `CASE` statement. + Case(CaseStatement), + /// An `IF` statement. + If(IfStatement), /// ```sql /// CALL /// ``` @@ -3940,6 +4120,12 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::Case(stmt) => { + write!(f, "{stmt}") + } + Statement::If(stmt) => { + write!(f, "{stmt}") + } Statement::AttachDatabase { schema_name, database_file_name, @@ -4942,18 +5128,14 @@ impl fmt::Display for Statement { write!(f, " {}", display_comma_separated(modes))?; } if !statements.is_empty() { - write!(f, " {}", display_separated(statements, "; "))?; - // We manually insert semicolon for the last statement, - // since display_separated doesn't handle that case. - write!(f, ";")?; + write!(f, " ")?; + format_statement_list(f, statements)?; } if let Some(exception_statements) = exception_statements { write!(f, " EXCEPTION WHEN ERROR THEN")?; if !exception_statements.is_empty() { - write!(f, " {}", display_separated(exception_statements, "; "))?; - // We manually insert semicolon for the last statement, - // since display_separated doesn't handle that case. - write!(f, ";")?; + write!(f, " ")?; + format_statement_list(f, exception_statements)?; } } if *has_end_keyword { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a4f5eb46..0ee11f23 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -22,20 +22,21 @@ use crate::tokenizer::Span; use super::{ dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterIndexOperation, AlterTableOperation, Array, Assignment, AssignmentTarget, CloseCursor, - ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConflictTarget, ConnectBy, - ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, - Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, - Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, - FunctionArguments, GroupByExpr, HavingBound, IlikeSelectItem, Insert, Interpolate, - InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, - Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, - PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem, - ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, - Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, - TableFactor, TableObject, TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, - Value, Values, ViewColumnDef, WildcardAdditionalOptions, With, WithFill, + AlterIndexOperation, AlterTableOperation, Array, Assignment, AssignmentTarget, CaseStatement, + CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatements, + ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, + CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, + ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, + FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, + IfStatement, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, + JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, + NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, + OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, + Query, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, + SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, + TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, + TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -334,6 +335,8 @@ impl Spanned for Statement { file_format: _, source, } => source.span(), + Statement::Case(stmt) => stmt.span(), + Statement::If(stmt) => stmt.span(), Statement::Call(function) => function.span(), Statement::Copy { source, @@ -732,6 +735,53 @@ impl Spanned for CreateIndex { } } +impl Spanned for CaseStatement { + fn span(&self) -> Span { + let CaseStatement { + match_expr, + when_blocks, + else_block, + has_end_case: _, + } = self; + + union_spans( + match_expr + .iter() + .map(|e| e.span()) + .chain(when_blocks.iter().map(|b| b.span())) + .chain(else_block.iter().flat_map(|e| e.iter().map(|s| s.span()))), + ) + } +} + +impl Spanned for IfStatement { + fn span(&self) -> Span { + let IfStatement { + if_block, + elseif_blocks, + else_block, + } = self; + + union_spans( + iter::once(if_block.span()) + .chain(elseif_blocks.iter().map(|b| b.span())) + .chain(else_block.iter().flat_map(|e| e.iter().map(|s| s.span()))), + ) + } +} + +impl Spanned for ConditionalStatements { + fn span(&self) -> Span { + let ConditionalStatements { + condition, + statements, + kind: _, + } = self; + + union_spans(iter::once(condition.span()).chain(statements.iter().map(|s| s.span()))) + } +} + /// # partial span /// /// Missing spans: diff --git a/src/keywords.rs b/src/keywords.rs index 195bbb17..47da1009 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -297,6 +297,7 @@ define_keywords!( ELEMENT, ELEMENTS, ELSE, + ELSEIF, EMPTY, ENABLE, ENABLE_SCHEMA_EVOLUTION, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 60e1c146..3adfe55e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -528,6 +528,14 @@ impl<'a> Parser<'a> { Keyword::DESCRIBE => self.parse_explain(DescribeAlias::Describe), Keyword::EXPLAIN => self.parse_explain(DescribeAlias::Explain), Keyword::ANALYZE => self.parse_analyze(), + Keyword::CASE => { + self.prev_token(); + self.parse_case_stmt() + } + Keyword::IF => { + self.prev_token(); + self.parse_if_stmt() + } Keyword::SELECT | Keyword::WITH | Keyword::VALUES | Keyword::FROM => { self.prev_token(); self.parse_query().map(Statement::Query) @@ -615,6 +623,102 @@ impl<'a> Parser<'a> { } } + /// Parse a `CASE` statement. + /// + /// See [Statement::Case] + pub fn parse_case_stmt(&mut self) -> Result { + self.expect_keyword_is(Keyword::CASE)?; + + let match_expr = if self.peek_keyword(Keyword::WHEN) { + None + } else { + Some(self.parse_expr()?) + }; + + self.expect_keyword_is(Keyword::WHEN)?; + let when_blocks = self.parse_keyword_separated(Keyword::WHEN, |parser| { + parser.parse_conditional_statements( + ConditionalStatementKind::When, + &[Keyword::WHEN, Keyword::ELSE, Keyword::END], + ) + })?; + + let else_block = if self.parse_keyword(Keyword::ELSE) { + Some(self.parse_statement_list(&[Keyword::END])?) + } else { + None + }; + + self.expect_keyword_is(Keyword::END)?; + let has_end_case = self.parse_keyword(Keyword::CASE); + + Ok(Statement::Case(CaseStatement { + match_expr, + when_blocks, + else_block, + has_end_case, + })) + } + + /// Parse an `IF` statement. + /// + /// See [Statement::If] + pub fn parse_if_stmt(&mut self) -> Result { + self.expect_keyword_is(Keyword::IF)?; + let if_block = self.parse_conditional_statements( + ConditionalStatementKind::If, + &[Keyword::ELSE, Keyword::ELSEIF, Keyword::END], + )?; + + let elseif_blocks = if self.parse_keyword(Keyword::ELSEIF) { + self.parse_keyword_separated(Keyword::ELSEIF, |parser| { + parser.parse_conditional_statements( + ConditionalStatementKind::ElseIf, + &[Keyword::ELSEIF, Keyword::ELSE, Keyword::END], + ) + })? + } else { + vec![] + }; + + let else_block = if self.parse_keyword(Keyword::ELSE) { + Some(self.parse_statement_list(&[Keyword::END])?) + } else { + None + }; + + self.expect_keywords(&[Keyword::END, Keyword::IF])?; + + Ok(Statement::If(IfStatement { + if_block, + elseif_blocks, + else_block, + })) + } + + /// Parses an expression and associated list of statements + /// belonging to a conditional statement like `IF` or `WHEN`. + /// + /// Example: + /// ```sql + /// IF condition THEN statement1; statement2; + /// ``` + fn parse_conditional_statements( + &mut self, + kind: ConditionalStatementKind, + terminal_keywords: &[Keyword], + ) -> Result { + let condition = self.parse_expr()?; + self.expect_keyword_is(Keyword::THEN)?; + let statements = self.parse_statement_list(terminal_keywords)?; + + Ok(ConditionalStatements { + condition, + statements, + kind, + }) + } + pub fn parse_comment(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d0edafb7..8c9cae83 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14179,6 +14179,120 @@ fn test_visit_order() { ); } +#[test] +fn parse_case_statement() { + let sql = "CASE 1 WHEN 2 THEN SELECT 1; SELECT 2; ELSE SELECT 3; END CASE"; + let Statement::Case(stmt) = verified_stmt(sql) else { + unreachable!() + }; + + assert_eq!(Some(Expr::value(number("1"))), stmt.match_expr); + assert_eq!(Expr::value(number("2")), stmt.when_blocks[0].condition); + assert_eq!(2, stmt.when_blocks[0].statements.len()); + assert_eq!(1, stmt.else_block.unwrap().len()); + + verified_stmt(concat!( + "CASE 1", + " WHEN a THEN", + " SELECT 1; SELECT 2; SELECT 3;", + " WHEN b THEN", + " SELECT 4; SELECT 5;", + " ELSE", + " SELECT 7; SELECT 8;", + " END CASE" + )); + verified_stmt(concat!( + "CASE 1", + " WHEN a THEN", + " SELECT 1; SELECT 2; SELECT 3;", + " WHEN b THEN", + " SELECT 4; SELECT 5;", + " END CASE" + )); + verified_stmt(concat!( + "CASE 1", + " WHEN a THEN", + " SELECT 1; SELECT 2; SELECT 3;", + " END CASE" + )); + verified_stmt(concat!( + "CASE 1", + " WHEN a THEN", + " SELECT 1; SELECT 2; SELECT 3;", + " END" + )); + + assert_eq!( + ParserError::ParserError("Expected: THEN, found: END".to_string()), + parse_sql_statements("CASE 1 WHEN a END").unwrap_err() + ); + assert_eq!( + ParserError::ParserError("Expected: WHEN, found: ELSE".to_string()), + parse_sql_statements("CASE 1 ELSE SELECT 1; END").unwrap_err() + ); +} + +#[test] +fn parse_if_statement() { + let sql = "IF 1 THEN SELECT 1; ELSEIF 2 THEN SELECT 2; ELSE SELECT 3; END IF"; + let Statement::If(stmt) = verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(Expr::value(number("1")), stmt.if_block.condition); + assert_eq!(Expr::value(number("2")), stmt.elseif_blocks[0].condition); + assert_eq!(1, stmt.else_block.unwrap().len()); + + verified_stmt(concat!( + "IF 1 THEN", + " SELECT 1;", + " SELECT 2;", + " SELECT 3;", + " ELSEIF 2 THEN", + " SELECT 4;", + " SELECT 5;", + " ELSEIF 3 THEN", + " SELECT 6;", + " SELECT 7;", + " ELSE", + " SELECT 8;", + " SELECT 9;", + " END IF" + )); + verified_stmt(concat!( + "IF 1 THEN", + " SELECT 1;", + " SELECT 2;", + " ELSE", + " SELECT 3;", + " SELECT 4;", + " END IF" + )); + verified_stmt(concat!( + "IF 1 THEN", + " SELECT 1;", + " SELECT 2;", + " SELECT 3;", + " ELSEIF 2 THEN", + " SELECT 3;", + " SELECT 4;", + " END IF" + )); + verified_stmt(concat!("IF 1 THEN", " SELECT 1;", " SELECT 2;", " END IF")); + verified_stmt(concat!( + "IF (1) THEN", + " SELECT 1;", + " SELECT 2;", + " END IF" + )); + verified_stmt("IF 1 THEN END IF"); + verified_stmt("IF 1 THEN SELECT 1; ELSEIF 1 THEN END IF"); + + assert_eq!( + ParserError::ParserError("Expected: IF, found: EOF".to_string()), + parse_sql_statements("IF 1 THEN SELECT 1; ELSEIF 1 THEN SELECT 2; END").unwrap_err() + ); +} + #[test] fn test_lambdas() { let dialects = all_dialects_where(|d| d.supports_lambda_functions()); From f81aed6359d9da35df434a6b4b0df07b6cffd5c5 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Fri, 14 Mar 2025 08:00:19 +0100 Subject: [PATCH 011/130] BigQuery: Add support for `CREATE SCHEMA` options (#1742) --- src/ast/mod.rs | 42 +++++++++++++++++++++++++++++++------ src/parser/mod.rs | 14 +++++++++++++ tests/sqlparser_common.rs | 5 +++++ tests/sqlparser_postgres.rs | 2 ++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 66fd4c6f..8c407921 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3450,6 +3450,22 @@ pub enum Statement { /// ` | AUTHORIZATION | AUTHORIZATION ` schema_name: SchemaName, if_not_exists: bool, + /// Schema options. + /// + /// ```sql + /// CREATE SCHEMA myschema OPTIONS(key1='value1'); + /// ``` + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_schema_statement) + options: Option>, + /// Default collation specification for the schema. + /// + /// ```sql + /// CREATE SCHEMA myschema DEFAULT COLLATE 'und:ci'; + /// ``` + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_schema_statement) + default_collate_spec: Option, }, /// ```sql /// CREATE DATABASE @@ -5177,12 +5193,26 @@ impl fmt::Display for Statement { Statement::CreateSchema { schema_name, if_not_exists, - } => write!( - f, - "CREATE SCHEMA {if_not_exists}{name}", - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - name = schema_name - ), + options, + default_collate_spec, + } => { + write!( + f, + "CREATE SCHEMA {if_not_exists}{name}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = schema_name + )?; + + if let Some(collate) = default_collate_spec { + write!(f, " DEFAULT COLLATE {collate}")?; + } + + if let Some(options) = options { + write!(f, " OPTIONS({})", display_comma_separated(options))?; + } + + Ok(()) + } Statement::Assert { condition, message } => { write!(f, "ASSERT {condition}")?; if let Some(m) = message { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3adfe55e..864fd579 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4731,9 +4731,23 @@ impl<'a> Parser<'a> { let schema_name = self.parse_schema_name()?; + let default_collate_spec = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::COLLATE]) { + Some(self.parse_expr()?) + } else { + None + }; + + let options = if self.peek_keyword(Keyword::OPTIONS) { + Some(self.parse_options(Keyword::OPTIONS)?) + } else { + None + }; + Ok(Statement::CreateSchema { schema_name, if_not_exists, + options, + default_collate_spec, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8c9cae83..c65cc6b5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4208,6 +4208,11 @@ fn parse_create_schema() { } _ => unreachable!(), } + + verified_stmt(r#"CREATE SCHEMA a.b.c OPTIONS(key1 = 'value1', key2 = 'value2')"#); + verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a OPTIONS(key1 = 'value1')"#); + verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a OPTIONS()"#); + verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a DEFAULT COLLATE 'und:ci' OPTIONS()"#); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1a98870f..e62f2359 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -988,6 +988,8 @@ fn parse_create_schema_if_not_exists() { Statement::CreateSchema { if_not_exists: true, schema_name, + options: _, + default_collate_spec: _, } => assert_eq!("schema_name", schema_name.to_string()), _ => unreachable!(), } From 10cf7c164ee0bae8a71e1d8f0af5851b96465692 Mon Sep 17 00:00:00 2001 From: Aleksei Piianin Date: Sat, 15 Mar 2025 07:07:07 +0100 Subject: [PATCH 012/130] Snowflake: Support dollar quoted comments (#1755) --- src/dialect/snowflake.rs | 5 +---- src/parser/mod.rs | 37 ++++++++++++++++++------------------ tests/sqlparser_snowflake.rs | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 72252b27..09a0e57c 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -644,10 +644,7 @@ pub fn parse_create_stage( // [ comment ] if parser.parse_keyword(Keyword::COMMENT) { parser.expect_token(&Token::Eq)?; - comment = Some(match parser.next_token().token { - Token::SingleQuotedString(word) => Ok(word), - _ => parser.expected("a comment statement", parser.peek_token()), - }?) + comment = Some(parser.parse_comment_value()?); } Ok(Statement::CreateStage { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 864fd579..e4c170ed 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5451,11 +5451,7 @@ impl<'a> Parser<'a> { && self.parse_keyword(Keyword::COMMENT) { self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(str) => Some(str), - _ => self.expected("string literal", next_token)?, - } + Some(self.parse_comment_value()?) } else { None }; @@ -7059,21 +7055,28 @@ impl<'a> Parser<'a> { pub fn parse_optional_inline_comment(&mut self) -> Result, ParserError> { let comment = if self.parse_keyword(Keyword::COMMENT) { let has_eq = self.consume_token(&Token::Eq); - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(str) => Some(if has_eq { - CommentDef::WithEq(str) - } else { - CommentDef::WithoutEq(str) - }), - _ => self.expected("comment", next_token)?, - } + let comment = self.parse_comment_value()?; + Some(if has_eq { + CommentDef::WithEq(comment) + } else { + CommentDef::WithoutEq(comment) + }) } else { None }; Ok(comment) } + pub fn parse_comment_value(&mut self) -> Result { + let next_token = self.next_token(); + let value = match next_token.token { + Token::SingleQuotedString(str) => str, + Token::DollarQuotedString(str) => str.value, + _ => self.expected("string literal", next_token)?, + }; + Ok(value) + } + pub fn parse_optional_procedure_parameters( &mut self, ) -> Result>, ParserError> { @@ -7209,11 +7212,7 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { Ok(Some(ColumnOption::NotNull)) } else if self.parse_keywords(&[Keyword::COMMENT]) { - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(value, ..) => Ok(Some(ColumnOption::Comment(value))), - _ => self.expected("string", next_token), - } + Ok(Some(ColumnOption::Comment(self.parse_comment_value()?))) } else if self.parse_keyword(Keyword::NULL) { Ok(Some(ColumnOption::Null)) } else if self.parse_keyword(Keyword::DEFAULT) { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b1d31e6d..f37b657e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -976,6 +976,21 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { } } +#[test] +fn parse_sf_create_table_or_view_with_dollar_quoted_comment() { + // Snowflake transforms dollar quoted comments into a common comment in DDL representation of creation + snowflake() + .one_statement_parses_to( + r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ("COL_1" COMMENT $$comment 1$$) COMMENT = $$view comment$$ AS (SELECT 1)"#, + r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ("COL_1" COMMENT 'comment 1') COMMENT = 'view comment' AS (SELECT 1)"# + ); + + snowflake().one_statement_parses_to( + r#"CREATE TABLE my_table (a STRING COMMENT $$comment 1$$) COMMENT = $$table comment$$"#, + r#"CREATE TABLE my_table (a STRING COMMENT 'comment 1') COMMENT = 'table comment'"#, + ); +} + #[test] fn test_sf_derived_table_in_parenthesis() { // Nesting a subquery in an extra set of parentheses is non-standard, From da5892802f181f1df3609104b238fa2ab0baadaf Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:22:37 +0200 Subject: [PATCH 013/130] Add LOCK operation for ALTER TABLE (#1768) --- src/ast/ddl.rs | 37 ++++++++++++++++++++++++++++++++ src/ast/mod.rs | 2 +- src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 18 ++++++++++++++++ tests/sqlparser_mysql.rs | 46 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 99d8521c..39e43ef1 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -288,6 +288,16 @@ pub enum AlterTableOperation { equals: bool, algorithm: AlterTableAlgorithm, }, + + /// `LOCK [=] { DEFAULT | NONE | SHARED | EXCLUSIVE }` + /// + /// [MySQL]-specific table alter lock. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + Lock { + equals: bool, + lock: AlterTableLock, + }, /// `AUTO_INCREMENT [=] ` /// /// [MySQL]-specific table option for raising current auto increment value. @@ -366,6 +376,30 @@ impl fmt::Display for AlterTableAlgorithm { } } +/// [MySQL] `ALTER TABLE` lock. +/// +/// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTableLock { + Default, + None, + Shared, + Exclusive, +} + +impl fmt::Display for AlterTableLock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Self::Default => "DEFAULT", + Self::None => "NONE", + Self::Shared => "SHARED", + Self::Exclusive => "EXCLUSIVE", + }) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -692,6 +726,9 @@ impl fmt::Display for AlterTableOperation { value ) } + AlterTableOperation::Lock { equals, lock } => { + write!(f, "LOCK {}{}", if *equals { "= " } else { "" }, lock) + } } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8c407921..69f2e57f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -48,7 +48,7 @@ pub use self::dcl::{ }; pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, - AlterTableAlgorithm, AlterTableOperation, AlterType, AlterTypeAddValue, + AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateFunction, Deduplicate, DeferrableInitial, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0ee11f23..783248a7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1119,6 +1119,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::ResumeRecluster => Span::empty(), AlterTableOperation::Algorithm { .. } => Span::empty(), AlterTableOperation::AutoIncrement { value, .. } => value.span(), + AlterTableOperation::Lock { .. } => Span::empty(), } } } diff --git a/src/keywords.rs b/src/keywords.rs index 47da1009..974230f1 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -797,6 +797,7 @@ define_keywords!( SETS, SETTINGS, SHARE, + SHARED, SHARING, SHOW, SIGNED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e4c170ed..0d8edc05 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8311,6 +8311,24 @@ impl<'a> Parser<'a> { AlterTableOperation::SuspendRecluster } else if self.parse_keywords(&[Keyword::RESUME, Keyword::RECLUSTER]) { AlterTableOperation::ResumeRecluster + } else if self.parse_keyword(Keyword::LOCK) { + let equals = self.consume_token(&Token::Eq); + let lock = match self.parse_one_of_keywords(&[ + Keyword::DEFAULT, + Keyword::EXCLUSIVE, + Keyword::NONE, + Keyword::SHARED, + ]) { + Some(Keyword::DEFAULT) => AlterTableLock::Default, + Some(Keyword::EXCLUSIVE) => AlterTableLock::Exclusive, + Some(Keyword::NONE) => AlterTableLock::None, + Some(Keyword::SHARED) => AlterTableLock::Shared, + _ => self.expected( + "DEFAULT, EXCLUSIVE, NONE or SHARED after LOCK [=]", + self.peek_token(), + )?, + }; + AlterTableOperation::Lock { equals, lock } } else if self.parse_keyword(Keyword::ALGORITHM) { let equals = self.consume_token(&Token::Eq); let algorithm = match self.parse_one_of_keywords(&[ diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a5633593..b6287d92 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2454,6 +2454,52 @@ fn parse_alter_table_with_algorithm() { mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = COPY"); } +#[test] +fn parse_alter_table_with_lock() { + let sql = "ALTER TABLE tab LOCK = SHARED"; + let expected_operation = AlterTableOperation::Lock { + equals: true, + lock: AlterTableLock::Shared, + }; + let operation = alter_table_op(mysql_and_generic().verified_stmt(sql)); + assert_eq!(expected_operation, operation); + + let sql = + "ALTER TABLE users DROP COLUMN password_digest, LOCK = EXCLUSIVE, RENAME COLUMN name TO username"; + let stmt = mysql_and_generic().verified_stmt(sql); + match stmt { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![ + AlterTableOperation::DropColumn { + column_name: Ident::new("password_digest"), + if_exists: false, + drop_behavior: None, + }, + AlterTableOperation::Lock { + equals: true, + lock: AlterTableLock::Exclusive, + }, + AlterTableOperation::RenameColumn { + old_column_name: Ident::new("name"), + new_column_name: Ident::new("username") + }, + ] + ) + } + _ => panic!("Unexpected statement {stmt}"), + } + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK DEFAULT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK SHARED"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK NONE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK EXCLUSIVE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK = DEFAULT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK = SHARED"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK = NONE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK = EXCLUSIVE"); +} + #[test] fn parse_alter_table_auto_increment() { let sql = "ALTER TABLE tab AUTO_INCREMENT = 42"; From e3e88290cd44df48e9bde2e931985193218449fc Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Tue, 18 Mar 2025 15:19:51 +0100 Subject: [PATCH 014/130] Add support for `RAISE` statement (#1766) --- src/ast/mod.rs | 56 +++++++++++++++++++++++++++++++++++++++ src/ast/spans.rs | 28 ++++++++++++++++---- src/keywords.rs | 2 ++ src/parser/mod.rs | 20 ++++++++++++++ tests/sqlparser_common.rs | 23 ++++++++++++++++ 5 files changed, 124 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 69f2e57f..6dc4f5b2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2256,6 +2256,57 @@ impl fmt::Display for ConditionalStatements { } } +/// A `RAISE` statement. +/// +/// Examples: +/// ```sql +/// RAISE USING MESSAGE = 'error'; +/// +/// RAISE myerror; +/// ``` +/// +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#raise) +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/raise) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct RaiseStatement { + pub value: Option, +} + +impl fmt::Display for RaiseStatement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let RaiseStatement { value } = self; + + write!(f, "RAISE")?; + if let Some(value) = value { + write!(f, " {value}")?; + } + + Ok(()) + } +} + +/// Represents the error value of a [RaiseStatement]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RaiseStatementValue { + /// `RAISE USING MESSAGE = 'error'` + UsingMessage(Expr), + /// `RAISE myerror` + Expr(Expr), +} + +impl fmt::Display for RaiseStatementValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RaiseStatementValue::Expr(expr) => write!(f, "{expr}"), + RaiseStatementValue::UsingMessage(expr) => write!(f, "USING MESSAGE = {expr}"), + } + } +} + /// Represents an expression assignment within a variable `DECLARE` statement. /// /// Examples: @@ -2827,6 +2878,8 @@ pub enum Statement { Case(CaseStatement), /// An `IF` statement. If(IfStatement), + /// A `RAISE` statement. + Raise(RaiseStatement), /// ```sql /// CALL /// ``` @@ -4142,6 +4195,9 @@ impl fmt::Display for Statement { Statement::If(stmt) => { write!(f, "{stmt}") } + Statement::Raise(stmt) => { + write!(f, "{stmt}") + } Statement::AttachDatabase { schema_name, database_file_name, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 783248a7..65d43c10 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -32,11 +32,11 @@ use super::{ JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, - Query, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, - SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, - TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, - TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, - WildcardAdditionalOptions, With, WithFill, + Query, RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, + ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, + Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, + TableFactor, TableObject, TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, + Value, Values, ViewColumnDef, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -337,6 +337,7 @@ impl Spanned for Statement { } => source.span(), Statement::Case(stmt) => stmt.span(), Statement::If(stmt) => stmt.span(), + Statement::Raise(stmt) => stmt.span(), Statement::Call(function) => function.span(), Statement::Copy { source, @@ -782,6 +783,23 @@ impl Spanned for ConditionalStatements { } } +impl Spanned for RaiseStatement { + fn span(&self) -> Span { + let RaiseStatement { value } = self; + + union_spans(value.iter().map(|value| value.span())) + } +} + +impl Spanned for RaiseStatementValue { + fn span(&self) -> Span { + match self { + RaiseStatementValue::UsingMessage(expr) => expr.span(), + RaiseStatementValue::Expr(expr) => expr.span(), + } + } +} + /// # partial span /// /// Missing spans: diff --git a/src/keywords.rs b/src/keywords.rs index 974230f1..7b9c8bf2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -533,6 +533,7 @@ define_keywords!( MEDIUMTEXT, MEMBER, MERGE, + MESSAGE, METADATA, METHOD, METRIC, @@ -695,6 +696,7 @@ define_keywords!( QUARTER, QUERY, QUOTE, + RAISE, RAISERROR, RANGE, RANK, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0d8edc05..ee50cd04 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -536,6 +536,10 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_if_stmt() } + Keyword::RAISE => { + self.prev_token(); + self.parse_raise_stmt() + } Keyword::SELECT | Keyword::WITH | Keyword::VALUES | Keyword::FROM => { self.prev_token(); self.parse_query().map(Statement::Query) @@ -719,6 +723,22 @@ impl<'a> Parser<'a> { }) } + /// Parse a `RAISE` statement. + /// + /// See [Statement::Raise] + pub fn parse_raise_stmt(&mut self) -> Result { + self.expect_keyword_is(Keyword::RAISE)?; + + let value = if self.parse_keywords(&[Keyword::USING, Keyword::MESSAGE]) { + self.expect_token(&Token::Eq)?; + Some(RaiseStatementValue::UsingMessage(self.parse_expr()?)) + } else { + self.maybe_parse(|parser| parser.parse_expr().map(RaiseStatementValue::Expr))? + }; + + Ok(Statement::Raise(RaiseStatement { value })) + } + pub fn parse_comment(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c65cc6b5..c8df7cab 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14298,6 +14298,29 @@ fn parse_if_statement() { ); } +#[test] +fn parse_raise_statement() { + let sql = "RAISE USING MESSAGE = 42"; + let Statement::Raise(stmt) = verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + Some(RaiseStatementValue::UsingMessage(Expr::value(number("42")))), + stmt.value + ); + + verified_stmt("RAISE USING MESSAGE = 'error'"); + verified_stmt("RAISE myerror"); + verified_stmt("RAISE 42"); + verified_stmt("RAISE using"); + verified_stmt("RAISE"); + + assert_eq!( + ParserError::ParserError("Expected: =, found: error".to_string()), + parse_sql_statements("RAISE USING MESSAGE error").unwrap_err() + ); +} + #[test] fn test_lambdas() { let dialects = all_dialects_where(|d| d.supports_lambda_functions()); From f487cbe00404454ce88a297bbe27dd98122f6fab Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Thu, 20 Mar 2025 07:52:56 +0200 Subject: [PATCH 015/130] Add GLOBAL context/modifier to SET statements (#1767) --- src/ast/mod.rs | 19 ++++++++++++------- src/parser/mod.rs | 29 +++++++++++++++++++---------- tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++------ tests/sqlparser_hive.rs | 9 +++++---- tests/sqlparser_mssql.rs | 2 +- tests/sqlparser_mysql.rs | 2 +- tests/sqlparser_postgres.rs | 18 ++++++++---------- 7 files changed, 72 insertions(+), 39 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6dc4f5b2..9f895ee6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2629,7 +2629,7 @@ pub enum Set { /// SQL Standard-style /// SET a = 1; SingleAssignment { - local: bool, + scope: ContextModifier, hivevar: bool, variable: ObjectName, values: Vec, @@ -2711,7 +2711,7 @@ impl Display for Set { role_name, } => { let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); - write!(f, "SET{context_modifier} ROLE {role_name}") + write!(f, "SET {context_modifier}ROLE {role_name}") } Self::SetSessionParam(kind) => write!(f, "SET {kind}"), Self::SetTransaction { @@ -2758,7 +2758,7 @@ impl Display for Set { Ok(()) } Set::SingleAssignment { - local, + scope, hivevar, variable, values, @@ -2766,7 +2766,7 @@ impl Display for Set { write!( f, "SET {}{}{} = {}", - if *local { "LOCAL " } else { "" }, + scope, if *hivevar { "HIVEVAR:" } else { "" }, variable, display_comma_separated(values) @@ -7955,7 +7955,7 @@ impl fmt::Display for FlushLocation { } } -/// Optional context modifier for statements that can be or `LOCAL`, or `SESSION`. +/// Optional context modifier for statements that can be or `LOCAL`, `GLOBAL`, or `SESSION`. #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7966,6 +7966,8 @@ pub enum ContextModifier { Local, /// `SESSION` identifier Session, + /// `GLOBAL` identifier + Global, } impl fmt::Display for ContextModifier { @@ -7975,10 +7977,13 @@ impl fmt::Display for ContextModifier { write!(f, "") } Self::Local => { - write!(f, " LOCAL") + write!(f, "LOCAL ") } Self::Session => { - write!(f, " SESSION") + write!(f, "SESSION ") + } + Self::Global => { + write!(f, "GLOBAL ") } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ee50cd04..dcf7a4a8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1819,6 +1819,15 @@ impl<'a> Parser<'a> { }) } + fn keyword_to_modifier(k: Option) -> ContextModifier { + match k { + Some(Keyword::LOCAL) => ContextModifier::Local, + Some(Keyword::GLOBAL) => ContextModifier::Global, + Some(Keyword::SESSION) => ContextModifier::Session, + _ => ContextModifier::None, + } + } + /// Check if the root is an identifier and all fields are identifiers. fn is_all_ident(root: &Expr, fields: &[AccessExpr]) -> bool { if !matches!(root, Expr::Identifier(_)) { @@ -11138,11 +11147,7 @@ impl<'a> Parser<'a> { /// Parse a `SET ROLE` statement. Expects SET to be consumed already. fn parse_set_role(&mut self, modifier: Option) -> Result { self.expect_keyword_is(Keyword::ROLE)?; - let context_modifier = match modifier { - Some(Keyword::LOCAL) => ContextModifier::Local, - Some(Keyword::SESSION) => ContextModifier::Session, - _ => ContextModifier::None, - }; + let context_modifier = Self::keyword_to_modifier(modifier); let role_name = if self.parse_keyword(Keyword::NONE) { None @@ -11214,8 +11219,12 @@ impl<'a> Parser<'a> { } fn parse_set(&mut self) -> Result { - let modifier = - self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::HIVEVAR]); + let modifier = self.parse_one_of_keywords(&[ + Keyword::SESSION, + Keyword::LOCAL, + Keyword::HIVEVAR, + Keyword::GLOBAL, + ]); if let Some(Keyword::HIVEVAR) = modifier { self.expect_token(&Token::Colon)?; @@ -11231,7 +11240,7 @@ impl<'a> Parser<'a> { { if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { return Ok(Set::SingleAssignment { - local: modifier == Some(Keyword::LOCAL), + scope: Self::keyword_to_modifier(modifier), hivevar: modifier == Some(Keyword::HIVEVAR), variable: ObjectName::from(vec!["TIMEZONE".into()]), values: self.parse_set_values(false)?, @@ -11321,7 +11330,7 @@ impl<'a> Parser<'a> { }?; Ok(Set::SingleAssignment { - local: modifier == Some(Keyword::LOCAL), + scope: Self::keyword_to_modifier(modifier), hivevar: modifier == Some(Keyword::HIVEVAR), variable, values, @@ -11349,7 +11358,7 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { let stmt = match variables { OneOrManyWithParens::One(var) => Set::SingleAssignment { - local: modifier == Some(Keyword::LOCAL), + scope: Self::keyword_to_modifier(modifier), hivevar: modifier == Some(Keyword::HIVEVAR), variable: var, values: self.parse_set_values(false)?, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c8df7cab..4ba8df7f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8627,12 +8627,12 @@ fn parse_set_transaction() { fn parse_set_variable() { match verified_stmt("SET SOMETHING = '1'") { Statement::Set(Set::SingleAssignment { - local, + scope, hivevar, variable, values, }) => { - assert!(!local); + assert_eq!(scope, ContextModifier::None); assert!(!hivevar); assert_eq!(variable, ObjectName::from(vec!["SOMETHING".into()])); assert_eq!( @@ -8645,6 +8645,26 @@ fn parse_set_variable() { _ => unreachable!(), } + match verified_stmt("SET GLOBAL VARIABLE = 'Value'") { + Statement::Set(Set::SingleAssignment { + scope, + hivevar, + variable, + values, + }) => { + assert_eq!(scope, ContextModifier::Global); + assert!(!hivevar); + assert_eq!(variable, ObjectName::from(vec!["VARIABLE".into()])); + assert_eq!( + values, + vec![Expr::Value( + (Value::SingleQuotedString("Value".into())).with_empty_span() + )] + ); + } + _ => unreachable!(), + } + let multi_variable_dialects = all_dialects_where(|d| d.supports_parenthesized_set_variables()); let sql = r#"SET (a, b, c) = (1, 2, 3)"#; match multi_variable_dialects.verified_stmt(sql) { @@ -8719,12 +8739,12 @@ fn parse_set_variable() { fn parse_set_role_as_variable() { match verified_stmt("SET role = 'foobar'") { Statement::Set(Set::SingleAssignment { - local, + scope, hivevar, variable, values, }) => { - assert!(!local); + assert_eq!(scope, ContextModifier::None); assert!(!hivevar); assert_eq!(variable, ObjectName::from(vec!["role".into()])); assert_eq!( @@ -8766,12 +8786,12 @@ fn parse_double_colon_cast_at_timezone() { fn parse_set_time_zone() { match verified_stmt("SET TIMEZONE = 'UTC'") { Statement::Set(Set::SingleAssignment { - local, + scope, hivevar, variable, values, }) => { - assert!(!local); + assert_eq!(scope, ContextModifier::None); assert!(!hivevar); assert_eq!(variable, ObjectName::from(vec!["TIMEZONE".into()])); assert_eq!( diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 56fe22a0..a9549cb6 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -21,9 +21,10 @@ //! is also tested (on the inputs it can handle). use sqlparser::ast::{ - ClusteredBy, CommentDef, CreateFunction, CreateFunctionBody, CreateFunctionUsing, CreateTable, - Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OrderByExpr, - OrderByOptions, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, Value, + ClusteredBy, CommentDef, ContextModifier, CreateFunction, CreateFunctionBody, + CreateFunctionUsing, CreateTable, Expr, Function, FunctionArgumentList, FunctionArguments, + Ident, ObjectName, OrderByExpr, OrderByOptions, SelectItem, Set, Statement, TableFactor, + UnaryOperator, Use, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::ParserError; @@ -369,7 +370,7 @@ fn set_statement_with_minus() { assert_eq!( hive().verified_stmt("SET hive.tez.java.opts = -Xmx4g"), Statement::Set(Set::SingleAssignment { - local: false, + scope: ContextModifier::None, hivevar: false, variable: ObjectName::from(vec![ Ident::new("hive"), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index af71d252..d4e5fa71 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1251,7 +1251,7 @@ fn parse_mssql_declare() { }] }, Statement::Set(Set::SingleAssignment { - local: false, + scope: ContextModifier::None, hivevar: false, variable: ObjectName::from(vec![Ident::new("@bar")]), values: vec![Expr::Value( diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b6287d92..88435149 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -618,7 +618,7 @@ fn parse_set_variables() { assert_eq!( mysql_and_generic().verified_stmt("SET LOCAL autocommit = 1"), Statement::Set(Set::SingleAssignment { - local: true, + scope: ContextModifier::Local, hivevar: false, variable: ObjectName::from(vec!["autocommit".into()]), values: vec![Expr::value(number("1"))], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e62f2359..cf66af74 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -988,8 +988,7 @@ fn parse_create_schema_if_not_exists() { Statement::CreateSchema { if_not_exists: true, schema_name, - options: _, - default_collate_spec: _, + .. } => assert_eq!("schema_name", schema_name.to_string()), _ => unreachable!(), } @@ -1433,7 +1432,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - local: false, + scope: ContextModifier::None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::Identifier(Ident { @@ -1448,7 +1447,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - local: false, + scope: ContextModifier::None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::Value( @@ -1461,7 +1460,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - local: false, + scope: ContextModifier::None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::value(number("0"))], @@ -1472,7 +1471,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - local: false, + scope: ContextModifier::None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::Identifier(Ident::new("DEFAULT"))], @@ -1483,7 +1482,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - local: true, + scope: ContextModifier::Local, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::Identifier("b".into())], @@ -1494,7 +1493,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - local: false, + scope: ContextModifier::None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a"), Ident::new("b"), Ident::new("c")]), values: vec![Expr::Identifier(Ident { @@ -1512,7 +1511,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - local: false, + scope: ContextModifier::None, hivevar: false, variable: ObjectName::from(vec![ Ident::new("hive"), @@ -1526,7 +1525,6 @@ fn parse_set() { ); pg_and_generic().one_statement_parses_to("SET a TO b", "SET a = b"); - pg_and_generic().one_statement_parses_to("SET SESSION a = b", "SET a = b"); assert_eq!( pg_and_generic().parse_sql_statements("SET"), From 939fbdd4f62014d478b624b4d0429fcd06d775a6 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 21 Mar 2025 22:34:43 -0700 Subject: [PATCH 016/130] Parse `SUBSTR` as alias for `SUBSTRING` (#1769) --- src/ast/mod.rs | 11 ++++++++++- src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 18 +++++++++++++++--- tests/sqlparser_common.rs | 3 +++ tests/sqlparser_mssql.rs | 1 + tests/sqlparser_mysql.rs | 1 + 7 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9f895ee6..b1bce3e9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -890,6 +890,10 @@ pub enum Expr { /// true if the expression is represented using the `SUBSTRING(expr, start, len)` syntax /// This flag is used for formatting. special: bool, + + /// true if the expression is represented using the `SUBSTR` shorthand + /// This flag is used for formatting. + shorthand: bool, }, /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) @@ -1719,8 +1723,13 @@ impl fmt::Display for Expr { substring_from, substring_for, special, + shorthand, } => { - write!(f, "SUBSTRING({expr}")?; + f.write_str("SUBSTR")?; + if !*shorthand { + f.write_str("ING")?; + } + write!(f, "({expr}")?; if let Some(from_part) = substring_from { if *special { write!(f, ", {from_part}")?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 65d43c10..11770d1b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1503,6 +1503,7 @@ impl Spanned for Expr { substring_from, substring_for, special: _, + shorthand: _, } => union_spans( core::iter::once(expr.span()) .chain(substring_from.as_ref().map(|i| i.span())) diff --git a/src/keywords.rs b/src/keywords.rs index 7b9c8bf2..349c9ffb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -841,6 +841,7 @@ define_keywords!( STRING, STRUCT, SUBMULTISET, + SUBSTR, SUBSTRING, SUBSTRING_REGEX, SUCCEEDS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dcf7a4a8..a43be5c6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1302,7 +1302,10 @@ impl<'a> Parser<'a> { Keyword::POSITION if self.peek_token_ref().token == Token::LParen => { Ok(Some(self.parse_position_expr(w.clone().into_ident(w_span))?)) } - Keyword::SUBSTRING => Ok(Some(self.parse_substring_expr()?)), + Keyword::SUBSTR | Keyword::SUBSTRING => { + self.prev_token(); + Ok(Some(self.parse_substring()?)) + } Keyword::OVERLAY => Ok(Some(self.parse_overlay_expr()?)), Keyword::TRIM => Ok(Some(self.parse_trim_expr()?)), Keyword::INTERVAL => Ok(Some(self.parse_interval()?)), @@ -2412,8 +2415,16 @@ impl<'a> Parser<'a> { } } - pub fn parse_substring_expr(&mut self) -> Result { - // PARSE SUBSTRING (EXPR [FROM 1] [FOR 3]) + // { SUBSTRING | SUBSTR } ( [FROM 1] [FOR 3]) + pub fn parse_substring(&mut self) -> Result { + let shorthand = match self.expect_one_of_keywords(&[Keyword::SUBSTR, Keyword::SUBSTRING])? { + Keyword::SUBSTR => true, + Keyword::SUBSTRING => false, + _ => { + self.prev_token(); + return self.expected("SUBSTR or SUBSTRING", self.peek_token()); + } + }; self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; let mut from_expr = None; @@ -2433,6 +2444,7 @@ impl<'a> Parser<'a> { substring_from: from_expr.map(Box::new), substring_for: to_expr.map(Box::new), special, + shorthand, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4ba8df7f..cf95e2ae 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7607,6 +7607,9 @@ fn parse_substring() { verified_stmt("SELECT SUBSTRING('1', 1, 3)"); verified_stmt("SELECT SUBSTRING('1', 1)"); verified_stmt("SELECT SUBSTRING('1' FOR 3)"); + verified_stmt("SELECT SUBSTRING('foo' FROM 1 FOR 2) FROM t"); + verified_stmt("SELECT SUBSTR('foo' FROM 1 FOR 2) FROM t"); + verified_stmt("SELECT SUBSTR('foo', 1, 2) FROM t"); } #[test] diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index d4e5fa71..4ea42efc 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1133,6 +1133,7 @@ fn parse_substring_in_select() { (number("1")).with_empty_span() ))), special: true, + shorthand: false, })], into: None, from: vec![TableWithJoins { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 88435149..2767a78c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2590,6 +2590,7 @@ fn parse_substring_in_select() { (number("1")).with_empty_span() ))), special: true, + shorthand: false, })], into: None, from: vec![TableWithJoins { From 3a8a3bb7a52c2a855e40ccbf88c3073abda1f1e7 Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Sat, 22 Mar 2025 07:38:00 +0200 Subject: [PATCH 017/130] SET statements: scope modifier for multiple assignments (#1772) --- src/ast/mod.rs | 28 ++++++--- src/dialect/generic.rs | 4 ++ src/parser/mod.rs | 119 +++++++++++++++++------------------- tests/sqlparser_common.rs | 43 +++++++++++-- tests/sqlparser_hive.rs | 9 ++- tests/sqlparser_mssql.rs | 2 +- tests/sqlparser_mysql.rs | 2 +- tests/sqlparser_postgres.rs | 20 +++--- 8 files changed, 133 insertions(+), 94 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b1bce3e9..3264cf03 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2638,7 +2638,7 @@ pub enum Set { /// SQL Standard-style /// SET a = 1; SingleAssignment { - scope: ContextModifier, + scope: Option, hivevar: bool, variable: ObjectName, values: Vec, @@ -2668,7 +2668,7 @@ pub enum Set { /// [4]: https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10004.htm SetRole { /// Non-ANSI optional identifier to inform if the role is defined inside the current session (`SESSION`) or transaction (`LOCAL`). - context_modifier: ContextModifier, + context_modifier: Option, /// Role name. If NONE is specified, then the current role name is removed. role_name: Option, }, @@ -2720,7 +2720,13 @@ impl Display for Set { role_name, } => { let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); - write!(f, "SET {context_modifier}ROLE {role_name}") + write!( + f, + "SET {modifier}ROLE {role_name}", + modifier = context_modifier + .map(|m| format!("{}", m)) + .unwrap_or_default() + ) } Self::SetSessionParam(kind) => write!(f, "SET {kind}"), Self::SetTransaction { @@ -2775,7 +2781,7 @@ impl Display for Set { write!( f, "SET {}{}{} = {}", - scope, + scope.map(|s| format!("{}", s)).unwrap_or_default(), if *hivevar { "HIVEVAR:" } else { "" }, variable, display_comma_separated(values) @@ -5736,13 +5742,20 @@ impl fmt::Display for SequenceOptions { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct SetAssignment { + pub scope: Option, pub name: ObjectName, pub value: Expr, } impl fmt::Display for SetAssignment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} = {}", self.name, self.value) + write!( + f, + "{}{} = {}", + self.scope.map(|s| format!("{}", s)).unwrap_or_default(), + self.name, + self.value + ) } } @@ -7969,8 +7982,6 @@ impl fmt::Display for FlushLocation { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ContextModifier { - /// No context defined. Each dialect defines the default in this scenario. - None, /// `LOCAL` identifier, usually related to transactional states. Local, /// `SESSION` identifier @@ -7982,9 +7993,6 @@ pub enum ContextModifier { impl fmt::Display for ContextModifier { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::None => { - write!(f, "") - } Self::Local => { write!(f, "LOCAL ") } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index c13d5aa6..92cfca8f 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -159,4 +159,8 @@ impl Dialect for GenericDialect { fn supports_set_names(&self) -> bool { true } + + fn supports_comma_separated_set_assignments(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a43be5c6..7d8417f3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1822,12 +1822,12 @@ impl<'a> Parser<'a> { }) } - fn keyword_to_modifier(k: Option) -> ContextModifier { + fn keyword_to_modifier(k: Keyword) -> Option { match k { - Some(Keyword::LOCAL) => ContextModifier::Local, - Some(Keyword::GLOBAL) => ContextModifier::Global, - Some(Keyword::SESSION) => ContextModifier::Session, - _ => ContextModifier::None, + Keyword::LOCAL => Some(ContextModifier::Local), + Keyword::GLOBAL => Some(ContextModifier::Global), + Keyword::SESSION => Some(ContextModifier::Session), + _ => None, } } @@ -11157,9 +11157,11 @@ impl<'a> Parser<'a> { } /// Parse a `SET ROLE` statement. Expects SET to be consumed already. - fn parse_set_role(&mut self, modifier: Option) -> Result { + fn parse_set_role( + &mut self, + modifier: Option, + ) -> Result { self.expect_keyword_is(Keyword::ROLE)?; - let context_modifier = Self::keyword_to_modifier(modifier); let role_name = if self.parse_keyword(Keyword::NONE) { None @@ -11167,7 +11169,7 @@ impl<'a> Parser<'a> { Some(self.parse_identifier()?) }; Ok(Statement::Set(Set::SetRole { - context_modifier, + context_modifier: modifier, role_name, })) } @@ -11203,46 +11205,52 @@ impl<'a> Parser<'a> { } } - fn parse_set_assignment( - &mut self, - ) -> Result<(OneOrManyWithParens, Expr), ParserError> { - let variables = if self.dialect.supports_parenthesized_set_variables() + fn parse_context_modifier(&mut self) -> Option { + let modifier = + self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::GLOBAL])?; + + Self::keyword_to_modifier(modifier) + } + + /// Parse a single SET statement assignment `var = expr`. + fn parse_set_assignment(&mut self) -> Result { + let scope = self.parse_context_modifier(); + + let name = if self.dialect.supports_parenthesized_set_variables() && self.consume_token(&Token::LParen) { - let vars = OneOrManyWithParens::Many( - self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())? - .into_iter() - .map(|ident| ObjectName::from(vec![ident])) - .collect(), - ); - self.expect_token(&Token::RParen)?; - vars + // Parenthesized assignments are handled in the `parse_set` function after + // trying to parse list of assignments using this function. + // If a dialect supports both, and we find a LParen, we early exit from this function. + self.expected("Unparenthesized assignment", self.peek_token())? } else { - OneOrManyWithParens::One(self.parse_object_name(false)?) + self.parse_object_name(false)? }; if !(self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO)) { return self.expected("assignment operator", self.peek_token()); } - let values = self.parse_expr()?; + let value = self.parse_expr()?; - Ok((variables, values)) + Ok(SetAssignment { scope, name, value }) } fn parse_set(&mut self) -> Result { - let modifier = self.parse_one_of_keywords(&[ - Keyword::SESSION, - Keyword::LOCAL, - Keyword::HIVEVAR, - Keyword::GLOBAL, - ]); + let hivevar = self.parse_keyword(Keyword::HIVEVAR); - if let Some(Keyword::HIVEVAR) = modifier { + // Modifier is either HIVEVAR: or a ContextModifier (LOCAL, SESSION, etc), not both + let scope = if !hivevar { + self.parse_context_modifier() + } else { + None + }; + + if hivevar { self.expect_token(&Token::Colon)?; } - if let Some(set_role_stmt) = self.maybe_parse(|parser| parser.parse_set_role(modifier))? { + if let Some(set_role_stmt) = self.maybe_parse(|parser| parser.parse_set_role(scope))? { return Ok(set_role_stmt); } @@ -11252,8 +11260,8 @@ impl<'a> Parser<'a> { { if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { return Ok(Set::SingleAssignment { - scope: Self::keyword_to_modifier(modifier), - hivevar: modifier == Some(Keyword::HIVEVAR), + scope, + hivevar, variable: ObjectName::from(vec!["TIMEZONE".into()]), values: self.parse_set_values(false)?, } @@ -11263,7 +11271,7 @@ impl<'a> Parser<'a> { // the assignment operator. It's originally PostgreSQL specific, // but we allow it for all the dialects return Ok(Set::SetTimeZone { - local: modifier == Some(Keyword::LOCAL), + local: scope == Some(ContextModifier::Local), value: self.parse_expr()?, } .into()); @@ -11311,41 +11319,26 @@ impl<'a> Parser<'a> { } if self.dialect.supports_comma_separated_set_assignments() { + if scope.is_some() { + self.prev_token(); + } + if let Some(assignments) = self .maybe_parse(|parser| parser.parse_comma_separated(Parser::parse_set_assignment))? { return if assignments.len() > 1 { - let assignments = assignments - .into_iter() - .map(|(var, val)| match var { - OneOrManyWithParens::One(v) => Ok(SetAssignment { - name: v, - value: val, - }), - OneOrManyWithParens::Many(_) => { - self.expected("List of single identifiers", self.peek_token()) - } - }) - .collect::>()?; - Ok(Set::MultipleAssignments { assignments }.into()) } else { - let (vars, values): (Vec<_>, Vec<_>) = assignments.into_iter().unzip(); - - let variable = match vars.into_iter().next() { - Some(OneOrManyWithParens::One(v)) => Ok(v), - Some(OneOrManyWithParens::Many(_)) => self.expected( - "Single assignment or list of assignments", - self.peek_token(), - ), - None => self.expected("At least one identifier", self.peek_token()), - }?; + let SetAssignment { scope, name, value } = + assignments.into_iter().next().ok_or_else(|| { + ParserError::ParserError("Expected at least one assignment".to_string()) + })?; Ok(Set::SingleAssignment { - scope: Self::keyword_to_modifier(modifier), - hivevar: modifier == Some(Keyword::HIVEVAR), - variable, - values, + scope, + hivevar, + variable: name, + values: vec![value], } .into()) }; @@ -11370,8 +11363,8 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { let stmt = match variables { OneOrManyWithParens::One(var) => Set::SingleAssignment { - scope: Self::keyword_to_modifier(modifier), - hivevar: modifier == Some(Keyword::HIVEVAR), + scope, + hivevar, variable: var, values: self.parse_set_values(false)?, }, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index cf95e2ae..a137291a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8635,7 +8635,7 @@ fn parse_set_variable() { variable, values, }) => { - assert_eq!(scope, ContextModifier::None); + assert_eq!(scope, None); assert!(!hivevar); assert_eq!(variable, ObjectName::from(vec!["SOMETHING".into()])); assert_eq!( @@ -8655,7 +8655,7 @@ fn parse_set_variable() { variable, values, }) => { - assert_eq!(scope, ContextModifier::Global); + assert_eq!(scope, Some(ContextModifier::Global)); assert!(!hivevar); assert_eq!(variable, ObjectName::from(vec!["VARIABLE".into()])); assert_eq!( @@ -8747,7 +8747,7 @@ fn parse_set_role_as_variable() { variable, values, }) => { - assert_eq!(scope, ContextModifier::None); + assert_eq!(scope, None); assert!(!hivevar); assert_eq!(variable, ObjectName::from(vec!["role".into()])); assert_eq!( @@ -8794,7 +8794,7 @@ fn parse_set_time_zone() { variable, values, }) => { - assert_eq!(scope, ContextModifier::None); + assert_eq!(scope, None); assert!(!hivevar); assert_eq!(variable, ObjectName::from(vec!["TIMEZONE".into()])); assert_eq!( @@ -14859,10 +14859,12 @@ fn parse_multiple_set_statements() -> Result<(), ParserError> { assignments, vec![ SetAssignment { + scope: None, name: ObjectName::from(vec!["@a".into()]), value: Expr::value(number("1")) }, SetAssignment { + scope: None, name: ObjectName::from(vec!["b".into()]), value: Expr::value(number("2")) } @@ -14872,6 +14874,39 @@ fn parse_multiple_set_statements() -> Result<(), ParserError> { _ => panic!("Expected SetVariable with 2 variables and 2 values"), }; + let stmt = dialects.verified_stmt("SET GLOBAL @a = 1, SESSION b = 2, LOCAL c = 3, d = 4"); + + match stmt { + Statement::Set(Set::MultipleAssignments { assignments }) => { + assert_eq!( + assignments, + vec![ + SetAssignment { + scope: Some(ContextModifier::Global), + name: ObjectName::from(vec!["@a".into()]), + value: Expr::value(number("1")) + }, + SetAssignment { + scope: Some(ContextModifier::Session), + name: ObjectName::from(vec!["b".into()]), + value: Expr::value(number("2")) + }, + SetAssignment { + scope: Some(ContextModifier::Local), + name: ObjectName::from(vec!["c".into()]), + value: Expr::value(number("3")) + }, + SetAssignment { + scope: None, + name: ObjectName::from(vec!["d".into()]), + value: Expr::value(number("4")) + } + ] + ); + } + _ => panic!("Expected MultipleAssignments with 4 scoped variables and 4 values"), + }; + Ok(()) } diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index a9549cb6..2af93db7 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -21,10 +21,9 @@ //! is also tested (on the inputs it can handle). use sqlparser::ast::{ - ClusteredBy, CommentDef, ContextModifier, CreateFunction, CreateFunctionBody, - CreateFunctionUsing, CreateTable, Expr, Function, FunctionArgumentList, FunctionArguments, - Ident, ObjectName, OrderByExpr, OrderByOptions, SelectItem, Set, Statement, TableFactor, - UnaryOperator, Use, Value, + ClusteredBy, CommentDef, CreateFunction, CreateFunctionBody, CreateFunctionUsing, CreateTable, + Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OrderByExpr, + OrderByOptions, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::ParserError; @@ -370,7 +369,7 @@ fn set_statement_with_minus() { assert_eq!( hive().verified_stmt("SET hive.tez.java.opts = -Xmx4g"), Statement::Set(Set::SingleAssignment { - scope: ContextModifier::None, + scope: None, hivevar: false, variable: ObjectName::from(vec![ Ident::new("hive"), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 4ea42efc..ca59b164 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1252,7 +1252,7 @@ fn parse_mssql_declare() { }] }, Statement::Set(Set::SingleAssignment { - scope: ContextModifier::None, + scope: None, hivevar: false, variable: ObjectName::from(vec![Ident::new("@bar")]), values: vec![Expr::Value( diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 2767a78c..d52619d5 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -618,7 +618,7 @@ fn parse_set_variables() { assert_eq!( mysql_and_generic().verified_stmt("SET LOCAL autocommit = 1"), Statement::Set(Set::SingleAssignment { - scope: ContextModifier::Local, + scope: Some(ContextModifier::Local), hivevar: false, variable: ObjectName::from(vec!["autocommit".into()]), values: vec![Expr::value(number("1"))], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index cf66af74..a6d65ec7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1432,7 +1432,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - scope: ContextModifier::None, + scope: None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::Identifier(Ident { @@ -1447,7 +1447,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - scope: ContextModifier::None, + scope: None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::Value( @@ -1460,7 +1460,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - scope: ContextModifier::None, + scope: None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::value(number("0"))], @@ -1471,7 +1471,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - scope: ContextModifier::None, + scope: None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::Identifier(Ident::new("DEFAULT"))], @@ -1482,7 +1482,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - scope: ContextModifier::Local, + scope: Some(ContextModifier::Local), hivevar: false, variable: ObjectName::from(vec![Ident::new("a")]), values: vec![Expr::Identifier("b".into())], @@ -1493,7 +1493,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - scope: ContextModifier::None, + scope: None, hivevar: false, variable: ObjectName::from(vec![Ident::new("a"), Ident::new("b"), Ident::new("c")]), values: vec![Expr::Identifier(Ident { @@ -1511,7 +1511,7 @@ fn parse_set() { assert_eq!( stmt, Statement::Set(Set::SingleAssignment { - scope: ContextModifier::None, + scope: None, hivevar: false, variable: ObjectName::from(vec![ Ident::new("hive"), @@ -1555,7 +1555,7 @@ fn parse_set_role() { assert_eq!( stmt, Statement::Set(Set::SetRole { - context_modifier: ContextModifier::Session, + context_modifier: Some(ContextModifier::Session), role_name: None, }) ); @@ -1566,7 +1566,7 @@ fn parse_set_role() { assert_eq!( stmt, Statement::Set(Set::SetRole { - context_modifier: ContextModifier::Local, + context_modifier: Some(ContextModifier::Local), role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\"'), @@ -1581,7 +1581,7 @@ fn parse_set_role() { assert_eq!( stmt, Statement::Set(Set::SetRole { - context_modifier: ContextModifier::None, + context_modifier: None, role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\''), From 53aba68e2dc758e591292115b1dd5faf71374346 Mon Sep 17 00:00:00 2001 From: tomershaniii <65544633+tomershaniii@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:13:11 +0200 Subject: [PATCH 018/130] Support qualified column names in `MATCH AGAINST` clause (#1774) --- src/ast/mod.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_mysql.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3264cf03..f187df99 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1027,7 +1027,7 @@ pub enum Expr { /// [(1)]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match MatchAgainst { /// `(, , ...)`. - columns: Vec, + columns: Vec, /// ``. match_value: Value, /// `` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7d8417f3..65d536f7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2704,7 +2704,7 @@ impl<'a> Parser<'a> { /// This method will raise an error if the column list is empty or with invalid identifiers, /// the match expression is not a literal string, or if the search modifier is not valid. pub fn parse_match_against(&mut self) -> Result { - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_qualified_column_list(Mandatory, false)?; self.expect_keyword_is(Keyword::AGAINST)?; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index d52619d5..3d318f70 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3454,3 +3454,32 @@ fn parse_cast_integers() { .run_parser_method("CAST(foo AS UNSIGNED INTEGER(3))", |p| p.parse_expr()) .expect_err("CAST doesn't allow display width"); } + +#[test] +fn parse_match_against_with_alias() { + let sql = "SELECT tbl.ProjectID FROM surveys.tbl1 AS tbl WHERE MATCH (tbl.ReferenceID) AGAINST ('AAA' IN BOOLEAN MODE)"; + match mysql().verified_stmt(sql) { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match select.selection { + Some(Expr::MatchAgainst { + columns, + match_value, + opt_search_modifier, + }) => { + assert_eq!( + columns, + vec![ObjectName::from(vec![ + Ident::new("tbl"), + Ident::new("ReferenceID") + ])] + ); + assert_eq!(match_value, Value::SingleQuotedString("AAA".to_owned())); + assert_eq!(opt_search_modifier, Some(SearchModifier::InBooleanMode)); + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + } +} From 62495f2f0d998690495f111063a0b0a3466ffa69 Mon Sep 17 00:00:00 2001 From: bar sela Date: Thu, 27 Mar 2025 23:48:57 +0200 Subject: [PATCH 019/130] Mysql: Add support for := operator (#1779) --- src/ast/operator.rs | 4 ++ src/dialect/mod.rs | 3 +- src/parser/mod.rs | 1 + tests/sqlparser_mysql.rs | 104 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 66a35fee..73fe9cf4 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -321,6 +321,9 @@ pub enum BinaryOperator { /// `~=` Same as? (PostgreSQL/Redshift geometric operator) /// See TildeEq, + /// ':=' Assignment Operator + /// See + Assignment, } impl fmt::Display for BinaryOperator { @@ -394,6 +397,7 @@ impl fmt::Display for BinaryOperator { BinaryOperator::QuestionDoublePipe => f.write_str("?||"), BinaryOperator::At => f.write_str("@"), BinaryOperator::TildeEq => f.write_str("~="), + BinaryOperator::Assignment => f.write_str(":="), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 8d4557e2..79184dd7 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -620,7 +620,8 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)), Token::Period => Ok(p!(Period)), - Token::Eq + Token::Assignment + | Token::Eq | Token::Lt | Token::LtEq | Token::Neq diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 65d536f7..adaae286 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3233,6 +3233,7 @@ impl<'a> Parser<'a> { let regular_binary_operator = match &tok.token { Token::Spaceship => Some(BinaryOperator::Spaceship), Token::DoubleEq => Some(BinaryOperator::Eq), + Token::Assignment => Some(BinaryOperator::Assignment), Token::Eq => Some(BinaryOperator::Eq), Token::Neq => Some(BinaryOperator::NotEq), Token::Gt => Some(BinaryOperator::Gt), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 3d318f70..1d4fd6a0 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3483,3 +3483,107 @@ fn parse_match_against_with_alias() { _ => unreachable!(), } } + +#[test] +fn test_variable_assignment_using_colon_equal() { + let sql_select = "SELECT @price := price, @tax := price * 0.1 FROM products WHERE id = 1"; + let stmt = mysql().verified_stmt(sql_select); + match stmt { + Statement::Query(query) => { + let select = query.body.as_select().unwrap(); + + assert_eq!( + select.projection, + vec![ + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "@price".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Assignment, + right: Box::new(Expr::Identifier(Ident { + value: "price".to_string(), + quote_style: None, + span: Span::empty(), + })), + }), + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "@tax".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Assignment, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "price".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Multiply, + right: Box::new(Expr::Value( + (test_utils::number("0.1")).with_empty_span() + )), + }), + }), + ] + ); + + assert_eq!( + select.selection, + Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "id".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value((test_utils::number("1")).with_empty_span())), + }) + ); + } + _ => panic!("Unexpected statement {stmt}"), + } + + let sql_update = + "UPDATE products SET price = @new_price := price * 1.1 WHERE category = 'Books'"; + let stmt = mysql().verified_stmt(sql_update); + + match stmt { + Statement::Update { assignments, .. } => { + assert_eq!( + assignments, + vec![Assignment { + target: AssignmentTarget::ColumnName(ObjectName(vec![ + ObjectNamePart::Identifier(Ident { + value: "price".to_string(), + quote_style: None, + span: Span::empty(), + }) + ])), + value: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "@new_price".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Assignment, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "price".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Multiply, + right: Box::new(Expr::Value( + (test_utils::number("1.1")).with_empty_span() + )), + }), + }, + }] + ) + } + _ => panic!("Unexpected statement {stmt}"), + } +} From be98b30eb3afc811e971856cbdf9809d32509af1 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 31 Mar 2025 21:46:59 +1100 Subject: [PATCH 020/130] Add cipherstash-proxy to list of users in README.md (#1782) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d18a76b5..5e8e460f 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ $ 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], and [ParadeDB]. +[Ballista], [GlueSQL], [Opteryx], [Polars], [PRQL], [Qrlew], [JumpWire], [ParadeDB] and [CipherStash Proxy]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. @@ -275,3 +275,4 @@ licensed as above, without any additional terms or conditions. [sql-standard]: https://en.wikipedia.org/wiki/ISO/IEC_9075 [`Dialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/trait.Dialect.html [`GenericDialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/struct.GenericDialect.html +[CipherStash Proxy]: https://github.com/cipherstash/proxy From 95d7b86da54c9cc4c75c47adffe73bf2cc38dd71 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Mon, 31 Mar 2025 21:47:53 +1100 Subject: [PATCH 021/130] Fix typos (#1785) --- src/dialect/snowflake.rs | 2 +- src/keywords.rs | 2 +- tests/sqlparser_common.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 09a0e57c..2b04f9e9 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -310,7 +310,7 @@ impl Dialect for SnowflakeDialect { } // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` - // which would give it a different meanins, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias + // which would give it a different meanings, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias Keyword::FETCH if parser.peek_keyword(Keyword::FIRST) || parser.peek_keyword(Keyword::NEXT) => { diff --git a/src/keywords.rs b/src/keywords.rs index 349c9ffb..a0c556d2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1084,7 +1084,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::END, ]; -// Global list of reserved keywords alloweed after FROM. +// Global list of reserved keywords allowed after FROM. // Parser should call Dialect::get_reserved_keyword_after_from // to allow for each dialect to customize the list. pub const RESERVED_FOR_TABLE_FACTOR: &[Keyword] = &[ diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a137291a..795dae4b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14528,7 +14528,7 @@ fn test_geometric_unary_operators() { } #[test] -fn test_geomtery_type() { +fn test_geometry_type() { let sql = "point '1,2'"; assert_eq!( all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), From 91327bb0c02a09e0b5c5322c813c4b2a3b564439 Mon Sep 17 00:00:00 2001 From: Roman Borschel Date: Mon, 31 Mar 2025 17:51:55 +0200 Subject: [PATCH 022/130] Add support for Databricks TIMESTAMP_NTZ. (#1781) Co-authored-by: Roman Borschel --- src/ast/data_type.rs | 5 +++++ src/keywords.rs | 1 + src/parser/mod.rs | 1 + tests/sqlparser_databricks.rs | 40 +++++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 57bc6744..dc523696 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -312,6 +312,10 @@ pub enum DataType { /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type Timestamp(Option, TimezoneInfo), + /// Databricks timestamp without time zone. See [1]. + /// + /// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type + TimestampNtz, /// Interval Interval, /// JSON type @@ -567,6 +571,7 @@ impl fmt::Display for DataType { DataType::Timestamp(precision, timezone_info) => { format_datetime_precision_and_tz(f, "TIMESTAMP", precision, timezone_info) } + DataType::TimestampNtz => write!(f, "TIMESTAMP_NTZ"), DataType::Datetime64(precision, timezone) => { format_clickhouse_datetime_precision_and_timezone( f, diff --git a/src/keywords.rs b/src/keywords.rs index a0c556d2..8609ec43 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -875,6 +875,7 @@ define_keywords!( TIME, TIMESTAMP, TIMESTAMPTZ, + TIMESTAMP_NTZ, TIMETZ, TIMEZONE, TIMEZONE_ABBR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index adaae286..2b61529f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9247,6 +9247,7 @@ impl<'a> Parser<'a> { self.parse_optional_precision()?, TimezoneInfo::Tz, )), + Keyword::TIMESTAMP_NTZ => Ok(DataType::TimestampNtz), Keyword::TIME => { let precision = self.parse_optional_precision()?; let tz = if self.parse_keyword(Keyword::WITH) { diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 3b36d7a1..88aae499 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -317,3 +317,43 @@ fn parse_databricks_struct_function() { }) ); } + +#[test] +fn data_type_timestamp_ntz() { + // Literal + assert_eq!( + databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), + Expr::TypedString { + data_type: DataType::TimestampNtz, + value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()) + } + ); + + // Cast + assert_eq!( + databricks().verified_expr("(created_at)::TIMESTAMP_NTZ"), + Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Nested(Box::new(Expr::Identifier( + "created_at".into() + )))), + data_type: DataType::TimestampNtz, + format: None + } + ); + + // Column definition + match databricks().verified_stmt("CREATE TABLE foo (x TIMESTAMP_NTZ)") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "x".into(), + data_type: DataType::TimestampNtz, + options: vec![], + }] + ); + } + s => panic!("Unexpected statement: {:?}", s), + } +} From 25bb87117583162069e8effd94ddc1b1c45bc476 Mon Sep 17 00:00:00 2001 From: Roman Borschel Date: Mon, 31 Mar 2025 17:53:56 +0200 Subject: [PATCH 023/130] Enable double-dot-notation for mssql. (#1787) Co-authored-by: Roman Borschel --- src/dialect/mssql.rs | 5 +++++ tests/sqlparser_mssql.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 3db34748..18a963a4 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -101,4 +101,9 @@ impl Dialect for MsSqlDialect { fn supports_nested_comments(&self) -> bool { true } + + /// See + fn supports_object_name_double_dot_notation(&self) -> bool { + true + } } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ca59b164..2bfc38a6 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1910,6 +1910,11 @@ fn parse_mssql_varbinary_max_length() { } } +#[test] +fn parse_mssql_table_identifier_with_default_schema() { + ms().verified_stmt("SELECT * FROM mydatabase..MyTable"); +} + fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } From 45420cedd60f64769a8fe9c8f960b9af4e0091fa Mon Sep 17 00:00:00 2001 From: Roman Borschel Date: Mon, 31 Mar 2025 18:09:58 +0200 Subject: [PATCH 024/130] Fix: Snowflake ALTER SESSION cannot be followed by other statements. (#1786) Co-authored-by: Roman Borschel --- src/dialect/snowflake.rs | 15 +++++++++------ tests/sqlparser_snowflake.rs | 11 +++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 2b04f9e9..d1a696a0 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1013,9 +1013,15 @@ fn parse_session_options( let mut options: Vec = Vec::new(); let empty = String::new; loop { - match parser.next_token().token { - Token::Comma => continue, + let next_token = parser.peek_token(); + match next_token.token { + Token::SemiColon | Token::EOF => break, + Token::Comma => { + parser.advance_token(); + continue; + } Token::Word(key) => { + parser.advance_token(); if set { let option = parse_option(parser, key)?; options.push(option); @@ -1028,10 +1034,7 @@ fn parse_session_options( } } _ => { - if parser.peek_token().token == Token::EOF { - break; - } - return parser.expected("another option", parser.peek_token()); + return parser.expected("another option or end of statement", next_token); } } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index f37b657e..097af346 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3505,3 +3505,14 @@ fn test_alter_session() { ); snowflake().one_statement_parses_to("ALTER SESSION UNSET a\nB", "ALTER SESSION UNSET a, B"); } + +#[test] +fn test_alter_session_followed_by_statement() { + let stmts = snowflake() + .parse_sql_statements("ALTER SESSION SET QUERY_TAG='hello'; SELECT 42") + .unwrap(); + match stmts[..] { + [Statement::AlterSession { .. }, Statement::Query { .. }] => {} + _ => panic!("Unexpected statements: {:?}", stmts), + } +} From 776b10afe608a88811b807ab795831d55f186ee3 Mon Sep 17 00:00:00 2001 From: LFC <990479+MichaelScofield@users.noreply.github.com> Date: Wed, 2 Apr 2025 01:06:19 +0800 Subject: [PATCH 025/130] Add GreptimeDB to the "Users" in README (#1788) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e8e460f..6acfbcef 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,8 @@ $ 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] and [CipherStash Proxy]. +[Ballista], [GlueSQL], [Opteryx], [Polars], [PRQL], [Qrlew], [JumpWire], [ParadeDB], [CipherStash Proxy], +and [GreptimeDB]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. @@ -276,3 +277,4 @@ licensed as above, without any additional terms or conditions. [`Dialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/trait.Dialect.html [`GenericDialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/struct.GenericDialect.html [CipherStash Proxy]: https://github.com/cipherstash/proxy +[GreptimeDB]: https://github.com/GreptimeTeam/greptimedb From 7efa686d7800dbe5a09a25c12980a7086a58d9aa Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:52:23 +0200 Subject: [PATCH 026/130] Extend snowflake grant options support (#1794) --- src/ast/mod.rs | 55 ++++++++++++++++++++++++++++++++---- src/keywords.rs | 1 + src/parser/mod.rs | 50 +++++++++++++++++++++++--------- tests/sqlparser_snowflake.rs | 39 ++++++++++++++++++++++++- 4 files changed, 126 insertions(+), 19 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f187df99..9456991e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6079,10 +6079,10 @@ pub enum Action { ManageReleases, ManageVersions, Modify { - modify_type: ActionModifyType, + modify_type: Option, }, Monitor { - monitor_type: ActionMonitorType, + monitor_type: Option, }, Operate, OverrideShareRestrictions, @@ -6115,7 +6115,7 @@ impl fmt::Display for Action { match self { Action::AddSearchOptimization => f.write_str("ADD SEARCH OPTIMIZATION")?, Action::Apply { apply_type } => write!(f, "APPLY {apply_type}")?, - Action::ApplyBudget => f.write_str("APPLY BUDGET")?, + Action::ApplyBudget => f.write_str("APPLYBUDGET")?, Action::AttachListing => f.write_str("ATTACH LISTING")?, Action::AttachPolicy => f.write_str("ATTACH POLICY")?, Action::Audit => f.write_str("AUDIT")?, @@ -6143,8 +6143,18 @@ impl fmt::Display for Action { Action::Manage { manage_type } => write!(f, "MANAGE {manage_type}")?, Action::ManageReleases => f.write_str("MANAGE RELEASES")?, Action::ManageVersions => f.write_str("MANAGE VERSIONS")?, - Action::Modify { modify_type } => write!(f, "MODIFY {modify_type}")?, - Action::Monitor { monitor_type } => write!(f, "MONITOR {monitor_type}")?, + Action::Modify { modify_type } => { + write!(f, "MODIFY")?; + if let Some(modify_type) = modify_type { + write!(f, " {modify_type}")?; + } + } + Action::Monitor { monitor_type } => { + write!(f, "MONITOR")?; + if let Some(monitor_type) = monitor_type { + write!(f, " {monitor_type}")? + } + } Action::Operate => f.write_str("OPERATE")?, Action::OverrideShareRestrictions => f.write_str("OVERRIDE SHARE RESTRICTIONS")?, Action::Ownership => f.write_str("OWNERSHIP")?, @@ -6462,6 +6472,20 @@ pub enum GrantObjects { Warehouses(Vec), /// Grant privileges on specific integrations Integrations(Vec), + /// Grant privileges on resource monitors + ResourceMonitors(Vec), + /// Grant privileges on users + Users(Vec), + /// Grant privileges on compute pools + ComputePools(Vec), + /// Grant privileges on connections + Connections(Vec), + /// Grant privileges on failover groups + FailoverGroup(Vec), + /// Grant privileges on replication group + ReplicationGroup(Vec), + /// Grant privileges on external volumes + ExternalVolumes(Vec), } impl fmt::Display for GrantObjects { @@ -6502,6 +6526,27 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::ResourceMonitors(objects) => { + write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects)) + } + GrantObjects::Users(objects) => { + write!(f, "USER {}", display_comma_separated(objects)) + } + GrantObjects::ComputePools(objects) => { + write!(f, "COMPUTE POOL {}", display_comma_separated(objects)) + } + GrantObjects::Connections(objects) => { + write!(f, "CONNECTION {}", display_comma_separated(objects)) + } + GrantObjects::FailoverGroup(objects) => { + write!(f, "FAILOVER GROUP {}", display_comma_separated(objects)) + } + GrantObjects::ReplicationGroup(objects) => { + write!(f, "REPLICATION GROUP {}", display_comma_separated(objects)) + } + GrantObjects::ExternalVolumes(objects) => { + write!(f, "EXTERNAL VOLUME {}", display_comma_separated(objects)) + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index 8609ec43..1aa2190c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -738,6 +738,7 @@ define_keywords!( REPLICATION, RESET, RESOLVE, + RESOURCE, RESPECT, RESTART, RESTRICT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2b61529f..40d6b0ac 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12875,6 +12875,26 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllSequencesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[Keyword::RESOURCE, Keyword::MONITOR]) { + Some(GrantObjects::ResourceMonitors(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) + } else if self.parse_keywords(&[Keyword::COMPUTE, Keyword::POOL]) { + Some(GrantObjects::ComputePools(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) + } else if self.parse_keywords(&[Keyword::FAILOVER, Keyword::GROUP]) { + Some(GrantObjects::FailoverGroup(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) + } else if self.parse_keywords(&[Keyword::REPLICATION, Keyword::GROUP]) { + Some(GrantObjects::ReplicationGroup(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) + } else if self.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) { + Some(GrantObjects::ExternalVolumes(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) } else { let object_type = self.parse_one_of_keywords(&[ Keyword::SEQUENCE, @@ -12888,6 +12908,8 @@ impl<'a> Parser<'a> { Keyword::VIEW, Keyword::WAREHOUSE, Keyword::INTEGRATION, + Keyword::USER, + Keyword::CONNECTION, ]); let objects = self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); @@ -12898,6 +12920,8 @@ impl<'a> Parser<'a> { Some(Keyword::WAREHOUSE) => Some(GrantObjects::Warehouses(objects?)), Some(Keyword::INTEGRATION) => Some(GrantObjects::Integrations(objects?)), Some(Keyword::VIEW) => Some(GrantObjects::Views(objects?)), + Some(Keyword::USER) => Some(GrantObjects::Users(objects?)), + Some(Keyword::CONNECTION) => Some(GrantObjects::Connections(objects?)), Some(Keyword::TABLE) | None => Some(GrantObjects::Tables(objects?)), _ => unreachable!(), } @@ -12983,10 +13007,10 @@ impl<'a> Parser<'a> { let manage_type = self.parse_action_manage_type()?; Ok(Action::Manage { manage_type }) } else if self.parse_keyword(Keyword::MODIFY) { - let modify_type = self.parse_action_modify_type()?; + let modify_type = self.parse_action_modify_type(); Ok(Action::Modify { modify_type }) } else if self.parse_keyword(Keyword::MONITOR) { - let monitor_type = self.parse_action_monitor_type()?; + let monitor_type = self.parse_action_monitor_type(); Ok(Action::Monitor { monitor_type }) } else if self.parse_keyword(Keyword::OPERATE) { Ok(Action::Operate) @@ -13127,29 +13151,29 @@ impl<'a> Parser<'a> { } } - fn parse_action_modify_type(&mut self) -> Result { + fn parse_action_modify_type(&mut self) -> Option { if self.parse_keywords(&[Keyword::LOG, Keyword::LEVEL]) { - Ok(ActionModifyType::LogLevel) + Some(ActionModifyType::LogLevel) } else if self.parse_keywords(&[Keyword::TRACE, Keyword::LEVEL]) { - Ok(ActionModifyType::TraceLevel) + Some(ActionModifyType::TraceLevel) } else if self.parse_keywords(&[Keyword::SESSION, Keyword::LOG, Keyword::LEVEL]) { - Ok(ActionModifyType::SessionLogLevel) + Some(ActionModifyType::SessionLogLevel) } else if self.parse_keywords(&[Keyword::SESSION, Keyword::TRACE, Keyword::LEVEL]) { - Ok(ActionModifyType::SessionTraceLevel) + Some(ActionModifyType::SessionTraceLevel) } else { - self.expected("GRANT MODIFY type", self.peek_token()) + None } } - fn parse_action_monitor_type(&mut self) -> Result { + fn parse_action_monitor_type(&mut self) -> Option { if self.parse_keyword(Keyword::EXECUTION) { - Ok(ActionMonitorType::Execution) + Some(ActionMonitorType::Execution) } else if self.parse_keyword(Keyword::SECURITY) { - Ok(ActionMonitorType::Security) + Some(ActionMonitorType::Security) } else if self.parse_keyword(Keyword::USAGE) { - Ok(ActionMonitorType::Usage) + Some(ActionMonitorType::Usage) } else { - self.expected("GRANT MONITOR type", self.peek_token()) + None } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 097af346..62e52e2d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3357,7 +3357,7 @@ fn test_timetravel_at_before() { } #[test] -fn test_grant_account_privileges() { +fn test_grant_account_global_privileges() { let privileges = vec![ "ALL", "ALL PRIVILEGES", @@ -3462,6 +3462,43 @@ fn test_grant_account_privileges() { } } +#[test] +fn test_grant_account_object_privileges() { + let privileges = vec![ + "ALL", + "ALL PRIVILEGES", + "APPLYBUDGET", + "MODIFY", + "MONITOR", + "USAGE", + "OPERATE", + ]; + + let objects_types = vec![ + "USER", + "RESOURCE MONITOR", + "WAREHOUSE", + "COMPUTE POOL", + "DATABASE", + "INTEGRATION", + "CONNECTION", + "FAILOVER GROUP", + "REPLICATION GROUP", + "EXTERNAL VOLUME", + ]; + + let with_grant_options = vec!["", " WITH GRANT OPTION"]; + + for t in &objects_types { + for p in &privileges { + for wgo in &with_grant_options { + let sql = format!("GRANT {p} ON {t} obj1 TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + } +} + #[test] fn test_grant_role_to() { snowflake_and_generic().verified_stmt("GRANT ROLE r1 TO ROLE r2"); From a847e4410572d55a1ff6f6bb01ba34b615f6f043 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Fri, 4 Apr 2025 12:34:18 +0200 Subject: [PATCH 027/130] Fix clippy lint on rust 1.86 (#1796) --- src/ast/ddl.rs | 2 +- src/ast/mod.rs | 12 ++++++------ src/dialect/snowflake.rs | 15 +++++++-------- src/keywords.rs | 12 ++++++------ tests/sqlparser_common.rs | 3 +-- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 39e43ef1..6a649b73 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -868,7 +868,7 @@ impl fmt::Display for AlterColumnOperation { AlterColumnOperation::SetDefault { value } => { write!(f, "SET DEFAULT {value}") } - AlterColumnOperation::DropDefault {} => { + AlterColumnOperation::DropDefault => { write!(f, "DROP DEFAULT") } AlterColumnOperation::SetDataType { data_type, using } => { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9456991e..07a22798 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -662,17 +662,17 @@ pub enum Expr { /// such as maps, arrays, and lists: /// - Array /// - A 1-dim array `a[1]` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]` /// - A 2-dim array `a[1][2]` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]` /// - Map or Struct (Bracket-style) /// - A map `a['field1']` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]` /// - A 2-dim map `a['field1']['field2']` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]` /// - Struct (Dot-style) (only effect when the chain contains both subscript and expr) /// - A struct access `a[field1].field2` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]` /// - If a struct access likes `a.field1.field2`, it will be represented by CompoundIdentifier([a, field1, field2]) CompoundFieldAccess { root: Box, @@ -7617,7 +7617,7 @@ impl fmt::Display for CopyTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use CopyTarget::*; match self { - Stdin { .. } => write!(f, "STDIN"), + Stdin => write!(f, "STDIN"), Stdout => write!(f, "STDOUT"), File { filename } => write!(f, "'{}'", value::escape_single_quote_string(filename)), Program { command } => write!( diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index d1a696a0..f303f821 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1038,14 +1038,13 @@ fn parse_session_options( } } } - options - .is_empty() - .then(|| { - Err(ParserError::ParserError( - "expected at least one option".to_string(), - )) - }) - .unwrap_or(Ok(options)) + if options.is_empty() { + Err(ParserError::ParserError( + "expected at least one option".to_string(), + )) + } else { + Ok(options) + } } /// Parses options provided within parentheses like: diff --git a/src/keywords.rs b/src/keywords.rs index 1aa2190c..bf1206f6 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -18,14 +18,14 @@ //! This module defines //! 1) a list of constants for every keyword //! 2) an `ALL_KEYWORDS` array with every keyword in it -//! This is not a list of *reserved* keywords: some of these can be -//! parsed as identifiers if the parser decides so. This means that -//! new keywords can be added here without affecting the parse result. +//! This is not a list of *reserved* keywords: some of these can be +//! parsed as identifiers if the parser decides so. This means that +//! new keywords can be added here without affecting the parse result. //! -//! As a matter of fact, most of these keywords are not used at all -//! and could be removed. +//! As a matter of fact, most of these keywords are not used at all +//! and could be removed. //! 3) a `RESERVED_FOR_TABLE_ALIAS` array with keywords reserved in a -//! "table alias" context. +//! "table alias" context. #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 795dae4b..9fe6eae7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14091,8 +14091,7 @@ fn test_table_sample() { #[test] fn overflow() { - let expr = std::iter::repeat("1") - .take(1000) + let expr = std::iter::repeat_n("1", 1000) .collect::>() .join(" + "); let sql = format!("SELECT {}", expr); From 3ed4ad9c66eca16b116be63afaa2d9e383bc2bc1 Mon Sep 17 00:00:00 2001 From: Roman Borschel Date: Fri, 4 Apr 2025 22:18:31 +0200 Subject: [PATCH 028/130] Allow single quotes in EXTRACT() for Redshift. (#1795) Co-authored-by: Roman Borschel --- src/dialect/redshift.rs | 4 ++++ tests/sqlparser_redshift.rs | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 25b8f164..d90eb6e7 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -121,4 +121,8 @@ impl Dialect for RedshiftSqlDialect { fn supports_array_typedef_with_brackets(&self) -> bool { true } + + fn allow_extract_single_quotes(&self) -> bool { + true + } } diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 7736735c..c75abe16 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -391,3 +391,9 @@ fn test_parse_nested_quoted_identifier() { .parse_sql_statements(r#"SELECT 1 AS ["1]"#) .is_err()); } + +#[test] +fn parse_extract_single_quotes() { + let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table"; + redshift().verified_stmt(&sql); +} From 610096cad8bdec90d86dc1f290dbac85cdc4e8ea Mon Sep 17 00:00:00 2001 From: DilovanCelik Date: Sat, 5 Apr 2025 18:37:28 +0000 Subject: [PATCH 029/130] MSSQL: Add support for functionality `MERGE` output clause (#1790) --- src/ast/mod.rs | 39 ++++++++++++++++++++++++++++- src/keywords.rs | 1 + src/parser/mod.rs | 50 ++++++++++++++++++++++++++----------- tests/sqlparser_bigquery.rs | 2 ++ tests/sqlparser_common.rs | 15 +++++++++++ tests/sqlparser_mssql.rs | 16 ++++++++++++ tests/sqlparser_redshift.rs | 2 +- 7 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 07a22798..8537bd85 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3817,6 +3817,7 @@ pub enum Statement { /// ``` /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + /// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16) Merge { /// optional INTO keyword into: bool, @@ -3828,6 +3829,8 @@ pub enum Statement { on: Box, /// Specifies the actions to perform when values match or do not match. clauses: Vec, + // Specifies the output to save changes in MSSQL + output: Option, }, /// ```sql /// CACHE [ FLAG ] TABLE [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ ] @@ -5407,6 +5410,7 @@ impl fmt::Display for Statement { source, on, clauses, + output, } => { write!( f, @@ -5414,7 +5418,11 @@ impl fmt::Display for Statement { int = if *into { " INTO" } else { "" } )?; write!(f, "ON {on} ")?; - write!(f, "{}", display_separated(clauses, " ")) + write!(f, "{}", display_separated(clauses, " "))?; + if let Some(output) = output { + write!(f, " {output}")?; + } + Ok(()) } Statement::Cache { table_name, @@ -7945,6 +7953,35 @@ impl Display for MergeClause { } } +/// A Output Clause in the end of a 'MERGE' Statement +/// +/// Example: +/// OUTPUT $action, deleted.* INTO dbo.temp_products; +/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct OutputClause { + pub select_items: Vec, + pub into_table: SelectInto, +} + +impl fmt::Display for OutputClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let OutputClause { + select_items, + into_table, + } = self; + + write!( + f, + "OUTPUT {} {}", + display_comma_separated(select_items), + into_table + ) + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/keywords.rs b/src/keywords.rs index bf1206f6..a1b4c0c3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -632,6 +632,7 @@ define_keywords!( ORGANIZATION, OUT, OUTER, + OUTPUT, OUTPUTFORMAT, OVER, OVERFLOW, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 40d6b0ac..50a9aae8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10910,18 +10910,7 @@ impl<'a> Parser<'a> { }; let into = if self.parse_keyword(Keyword::INTO) { - let temporary = self - .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) - .is_some(); - let unlogged = self.parse_keyword(Keyword::UNLOGGED); - let table = self.parse_keyword(Keyword::TABLE); - let name = self.parse_object_name(false)?; - Some(SelectInto { - temporary, - unlogged, - table, - name, - }) + Some(self.parse_select_into()?) } else { None }; @@ -14513,10 +14502,9 @@ impl<'a> Parser<'a> { pub fn parse_merge_clauses(&mut self) -> Result, ParserError> { let mut clauses = vec![]; loop { - if self.peek_token() == Token::EOF || self.peek_token() == Token::SemiColon { + if !(self.parse_keyword(Keyword::WHEN)) { break; } - self.expect_keyword_is(Keyword::WHEN)?; let mut clause_kind = MergeClauseKind::Matched; if self.parse_keyword(Keyword::NOT) { @@ -14610,6 +14598,34 @@ impl<'a> Parser<'a> { Ok(clauses) } + fn parse_output(&mut self) -> Result { + self.expect_keyword_is(Keyword::OUTPUT)?; + let select_items = self.parse_projection()?; + self.expect_keyword_is(Keyword::INTO)?; + let into_table = self.parse_select_into()?; + + Ok(OutputClause { + select_items, + into_table, + }) + } + + fn parse_select_into(&mut self) -> Result { + let temporary = self + .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) + .is_some(); + let unlogged = self.parse_keyword(Keyword::UNLOGGED); + let table = self.parse_keyword(Keyword::TABLE); + let name = self.parse_object_name(false)?; + + Ok(SelectInto { + temporary, + unlogged, + table, + name, + }) + } + pub fn parse_merge(&mut self) -> Result { let into = self.parse_keyword(Keyword::INTO); @@ -14620,6 +14636,11 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::ON)?; let on = self.parse_expr()?; let clauses = self.parse_merge_clauses()?; + let output = if self.peek_keyword(Keyword::OUTPUT) { + Some(self.parse_output()?) + } else { + None + }; Ok(Statement::Merge { into, @@ -14627,6 +14648,7 @@ impl<'a> Parser<'a> { source, on: Box::new(on), clauses, + output, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3037d4ae..5eb30d15 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1735,6 +1735,7 @@ fn parse_merge() { }, ], }; + match bigquery_and_generic().verified_stmt(sql) { Statement::Merge { into, @@ -1742,6 +1743,7 @@ fn parse_merge() { source, on, clauses, + .. } => { assert!(!into); assert_eq!( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9fe6eae7..36533217 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9359,6 +9359,7 @@ fn parse_merge() { source, on, clauses, + .. }, Statement::Merge { into: no_into, @@ -9366,6 +9367,7 @@ fn parse_merge() { source: source_no_into, on: on_no_into, clauses: clauses_no_into, + .. }, ) => { assert!(into); @@ -9558,6 +9560,19 @@ fn parse_merge() { verified_stmt(sql); } +#[test] +fn test_merge_with_output() { + let sql = "MERGE INTO target_table USING source_table \ + ON target_table.id = source_table.oooid \ + WHEN MATCHED THEN \ + UPDATE SET target_table.description = source_table.description \ + WHEN NOT MATCHED THEN \ + INSERT (ID, description) VALUES (source_table.id, source_table.description) \ + OUTPUT inserted.* INTO log_target"; + + verified_stmt(sql); +} + #[test] fn test_merge_into_using_table() { let sql = "MERGE INTO target_table USING source_table \ diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2bfc38a6..5d76fd01 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1921,3 +1921,19 @@ fn ms() -> TestedDialects { fn ms_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } + +#[test] +fn parse_mssql_merge_with_output() { + let stmt = "MERGE dso.products AS t \ + USING dsi.products AS \ + s ON s.ProductID = t.ProductID \ + WHEN MATCHED AND \ + NOT (t.ProductName = s.ProductName OR (ISNULL(t.ProductName, s.ProductName) IS NULL)) \ + THEN UPDATE SET t.ProductName = s.ProductName \ + WHEN NOT MATCHED BY TARGET \ + THEN INSERT (ProductID, ProductName) \ + VALUES (s.ProductID, s.ProductName) \ + WHEN NOT MATCHED BY SOURCE THEN DELETE \ + OUTPUT $action, deleted.ProductID INTO dsi.temp_products"; + ms_and_generic().verified_stmt(stmt); +} diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index c75abe16..060e3853 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -395,5 +395,5 @@ fn test_parse_nested_quoted_identifier() { #[test] fn parse_extract_single_quotes() { let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table"; - redshift().verified_stmt(&sql); + redshift().verified_stmt(sql); } From 4deed260061c625d97cde1ac9986697ad2f53cb5 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Sat, 5 Apr 2025 22:41:39 +0400 Subject: [PATCH 030/130] Support additional DuckDB integer types such as HUGEINT, UHUGEINT, etc (#1797) Co-authored-by: Alexander Beedie --- src/ast/data_type.rs | 391 +++++++++++++++++++++----------------- src/ast/ddl.rs | 10 +- src/ast/mod.rs | 30 +-- src/dialect/mod.rs | 2 +- src/keywords.rs | 5 + src/parser/mod.rs | 11 +- tests/sqlparser_duckdb.rs | 26 +++ 7 files changed, 274 insertions(+), 201 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index dc523696..52919de8 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -36,7 +36,7 @@ pub enum EnumMember { Name(String), /// ClickHouse allows to specify an integer value for each enum value. /// - /// [clickhouse](https://clickhouse.com/docs/en/sql-reference/data-types/enum) + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/data-types/enum) NamedValue(String, Expr), } @@ -45,270 +45,289 @@ pub enum EnumMember { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum DataType { - /// Table type in [postgresql]. e.g. CREATE FUNCTION RETURNS TABLE(...) + /// Table type in [PostgreSQL], e.g. CREATE FUNCTION RETURNS TABLE(...). /// - /// [postgresql]: https://www.postgresql.org/docs/15/sql-createfunction.html + /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html Table(Vec), - /// Fixed-length character type e.g. CHARACTER(10) + /// Fixed-length character type, e.g. CHARACTER(10). Character(Option), - /// Fixed-length char type e.g. CHAR(10) + /// Fixed-length char type, e.g. CHAR(10). Char(Option), - /// Character varying type e.g. CHARACTER VARYING(10) + /// Character varying type, e.g. CHARACTER VARYING(10). CharacterVarying(Option), - /// Char varying type e.g. CHAR VARYING(10) + /// Char varying type, e.g. CHAR VARYING(10). CharVarying(Option), - /// Variable-length character type e.g. VARCHAR(10) + /// Variable-length character type, e.g. VARCHAR(10). Varchar(Option), - /// Variable-length character type e.g. NVARCHAR(10) + /// Variable-length character type, e.g. NVARCHAR(10). Nvarchar(Option), - /// Uuid type + /// Uuid type. Uuid, - /// Large character object with optional length e.g. CHARACTER LARGE OBJECT, CHARACTER LARGE OBJECT(1000), [standard] + /// Large character object with optional length, + /// e.g. CHARACTER LARGE OBJECT, CHARACTER LARGE OBJECT(1000), [SQL Standard]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type CharacterLargeObject(Option), - /// Large character object with optional length e.g. CHAR LARGE OBJECT, CHAR LARGE OBJECT(1000), [standard] + /// Large character object with optional length, + /// e.g. CHAR LARGE OBJECT, CHAR LARGE OBJECT(1000), [SQL Standard]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type CharLargeObject(Option), - /// Large character object with optional length e.g. CLOB, CLOB(1000), [standard] + /// Large character object with optional length, + /// e.g. CLOB, CLOB(1000), [SQL Standard]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type /// [Oracle]: https://docs.oracle.com/javadb/10.10.1.2/ref/rrefclob.html Clob(Option), - /// Fixed-length binary type with optional length e.g. [standard], [MS SQL Server] + /// Fixed-length binary type with optional length, + /// see [SQL Standard], [MS SQL Server]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type /// [MS SQL Server]: https://learn.microsoft.com/pt-br/sql/t-sql/data-types/binary-and-varbinary-transact-sql?view=sql-server-ver16 Binary(Option), - /// Variable-length binary with optional length type e.g. [standard], [MS SQL Server] + /// Variable-length binary with optional length type, + /// see [SQL Standard], [MS SQL Server]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type /// [MS SQL Server]: https://learn.microsoft.com/pt-br/sql/t-sql/data-types/binary-and-varbinary-transact-sql?view=sql-server-ver16 Varbinary(Option), - /// Large binary object with optional length e.g. BLOB, BLOB(1000), [standard], [Oracle] + /// Large binary object with optional length, + /// see [SQL Standard], [Oracle]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-large-object-string-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-large-object-string-type /// [Oracle]: https://docs.oracle.com/javadb/10.8.3.0/ref/rrefblob.html Blob(Option), - /// [MySQL] blob with up to 2**8 bytes + /// [MySQL] blob with up to 2**8 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html TinyBlob, - /// [MySQL] blob with up to 2**24 bytes + /// [MySQL] blob with up to 2**24 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html MediumBlob, - /// [MySQL] blob with up to 2**32 bytes + /// [MySQL] blob with up to 2**32 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html LongBlob, /// Variable-length binary data with optional length. /// - /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#bytes_type + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#bytes_type Bytes(Option), - /// Numeric type with optional precision and scale e.g. NUMERIC(10,2), [standard][1] + /// Numeric type with optional precision and scale, e.g. NUMERIC(10,2), [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type Numeric(ExactNumberInfo), - /// Decimal type with optional precision and scale e.g. DECIMAL(10,2), [standard][1] + /// Decimal type with optional precision and scale, e.g. DECIMAL(10,2), [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type Decimal(ExactNumberInfo), - /// [BigNumeric] type used in BigQuery + /// [BigNumeric] type used in BigQuery. /// /// [BigNumeric]: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#bignumeric_literals BigNumeric(ExactNumberInfo), - /// This is alias for `BigNumeric` type used in BigQuery + /// This is alias for `BigNumeric` type used in BigQuery. /// /// [BigDecimal]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#decimal_types BigDecimal(ExactNumberInfo), - /// Dec type with optional precision and scale e.g. DEC(10,2), [standard][1] + /// Dec type with optional precision and scale, e.g. DEC(10,2), [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type Dec(ExactNumberInfo), - /// Floating point with optional precision e.g. FLOAT(8) + /// Floating point with optional precision, e.g. FLOAT(8). Float(Option), - /// Tiny integer with optional display width e.g. TINYINT or TINYINT(3) + /// Tiny integer with optional display width, e.g. TINYINT or TINYINT(3). TinyInt(Option), - /// Unsigned tiny integer with optional display width e.g. TINYINT UNSIGNED or TINYINT(3) UNSIGNED + /// Unsigned tiny integer with optional display width, + /// e.g. TINYINT UNSIGNED or TINYINT(3) UNSIGNED. TinyIntUnsigned(Option), - /// Int2 as alias for SmallInt in [postgresql] - /// Note: Int2 mean 2 bytes in postgres (not 2 bits) - /// Int2 with optional display width e.g. INT2 or INT2(5) + /// Unsigned tiny integer, e.g. UTINYINT + UTinyInt, + /// Int2 is an alias for SmallInt in [PostgreSQL]. + /// Note: Int2 means 2 bytes in PostgreSQL (not 2 bits). + /// Int2 with optional display width, e.g. INT2 or INT2(5). /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Int2(Option), - /// Unsigned Int2 with optional display width e.g. INT2 UNSIGNED or INT2(5) UNSIGNED + /// Unsigned Int2 with optional display width, e.g. INT2 UNSIGNED or INT2(5) UNSIGNED. Int2Unsigned(Option), - /// Small integer with optional display width e.g. SMALLINT or SMALLINT(5) + /// Small integer with optional display width, e.g. SMALLINT or SMALLINT(5). SmallInt(Option), - /// Unsigned small integer with optional display width e.g. SMALLINT UNSIGNED or SMALLINT(5) UNSIGNED + /// Unsigned small integer with optional display width, + /// e.g. SMALLINT UNSIGNED or SMALLINT(5) UNSIGNED. SmallIntUnsigned(Option), - /// MySQL medium integer ([1]) with optional display width e.g. MEDIUMINT or MEDIUMINT(5) + /// Unsigned small integer, e.g. USMALLINT. + USmallInt, + /// MySQL medium integer ([1]) with optional display width, + /// e.g. MEDIUMINT or MEDIUMINT(5). /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/integer-types.html MediumInt(Option), - /// Unsigned medium integer ([1]) with optional display width e.g. MEDIUMINT UNSIGNED or MEDIUMINT(5) UNSIGNED + /// Unsigned medium integer ([1]) with optional display width, + /// e.g. MEDIUMINT UNSIGNED or MEDIUMINT(5) UNSIGNED. /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/integer-types.html MediumIntUnsigned(Option), - /// Int with optional display width e.g. INT or INT(11) + /// Int with optional display width, e.g. INT or INT(11). Int(Option), - /// Int4 as alias for Integer in [postgresql] - /// Note: Int4 mean 4 bytes in postgres (not 4 bits) - /// Int4 with optional display width e.g. Int4 or Int4(11) + /// Int4 is an alias for Integer in [PostgreSQL]. + /// Note: Int4 means 4 bytes in PostgreSQL (not 4 bits). + /// Int4 with optional display width, e.g. Int4 or Int4(11). /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Int4(Option), - /// Int8 as alias for Bigint in [postgresql] and integer type in [clickhouse] - /// Note: Int8 mean 8 bytes in [postgresql] (not 8 bits) - /// Int8 with optional display width e.g. INT8 or INT8(11) - /// Note: Int8 mean 8 bits in [clickhouse] + /// Int8 is an alias for BigInt in [PostgreSQL] and Integer type in [ClickHouse]. + /// Int8 with optional display width, e.g. INT8 or INT8(11). + /// Note: Int8 means 8 bytes in [PostgreSQL], but 8 bits in [ClickHouse]. /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int8(Option), - /// Integer type in [clickhouse] - /// Note: Int16 mean 16 bits in [clickhouse] + /// Integer type in [ClickHouse]. + /// Note: Int16 means 16 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int16, - /// Integer type in [clickhouse] - /// Note: Int16 mean 32 bits in [clickhouse] + /// Integer type in [ClickHouse]. + /// Note: Int32 means 32 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int32, - /// Integer type in [bigquery], [clickhouse] + /// Integer type in [BigQuery], [ClickHouse]. /// - /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#integer_types - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#integer_types + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int64, - /// Integer type in [clickhouse] - /// Note: Int128 mean 128 bits in [clickhouse] + /// Integer type in [ClickHouse]. + /// Note: Int128 means 128 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int128, - /// Integer type in [clickhouse] - /// Note: Int256 mean 256 bits in [clickhouse] + /// Integer type in [ClickHouse]. + /// Note: Int256 means 256 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int256, - /// Integer with optional display width e.g. INTEGER or INTEGER(11) + /// Integer with optional display width, e.g. INTEGER or INTEGER(11). Integer(Option), - /// Unsigned int with optional display width e.g. INT UNSIGNED or INT(11) UNSIGNED + /// Unsigned int with optional display width, e.g. INT UNSIGNED or INT(11) UNSIGNED. IntUnsigned(Option), - /// Unsigned int4 with optional display width e.g. INT4 UNSIGNED or INT4(11) UNSIGNED + /// Unsigned int4 with optional display width, e.g. INT4 UNSIGNED or INT4(11) UNSIGNED. Int4Unsigned(Option), - /// Unsigned integer with optional display width e.g. INTEGER UNSIGNED or INTEGER(11) UNSIGNED + /// Unsigned integer with optional display width, e.g. INTEGER UNSIGNED or INTEGER(11) UNSIGNED. IntegerUnsigned(Option), - /// Unsigned integer type in [clickhouse] - /// Note: UInt8 mean 8 bits in [clickhouse] + /// 128-bit integer type, e.g. HUGEINT. + HugeInt, + /// Unsigned 128-bit integer type, e.g. UHUGEINT. + UHugeInt, + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt8 means 8 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt8, - /// Unsigned integer type in [clickhouse] - /// Note: UInt16 mean 16 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt16 means 16 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt16, - /// Unsigned integer type in [clickhouse] - /// Note: UInt32 mean 32 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt32 means 32 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt32, - /// Unsigned integer type in [clickhouse] - /// Note: UInt64 mean 64 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt64 means 64 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt64, - /// Unsigned integer type in [clickhouse] - /// Note: UInt128 mean 128 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt128 means 128 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt128, - /// Unsigned integer type in [clickhouse] - /// Note: UInt256 mean 256 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt256 means 256 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt256, - /// Big integer with optional display width e.g. BIGINT or BIGINT(20) + /// Big integer with optional display width, e.g. BIGINT or BIGINT(20). BigInt(Option), - /// Unsigned big integer with optional display width e.g. BIGINT UNSIGNED or BIGINT(20) UNSIGNED + /// Unsigned big integer with optional display width, e.g. BIGINT UNSIGNED or BIGINT(20) UNSIGNED. BigIntUnsigned(Option), - /// Unsigned Int8 with optional display width e.g. INT8 UNSIGNED or INT8(11) UNSIGNED + /// Unsigned big integer, e.g. UBIGINT. + UBigInt, + /// Unsigned Int8 with optional display width, e.g. INT8 UNSIGNED or INT8(11) UNSIGNED. Int8Unsigned(Option), - /// Signed integer as used in [MySQL CAST] target types, without optional `INTEGER` suffix: - /// `SIGNED` + /// Signed integer as used in [MySQL CAST] target types, without optional `INTEGER` suffix, + /// e.g. `SIGNED` /// /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html Signed, - /// Signed integer as used in [MySQL CAST] target types, with optional `INTEGER` suffix: - /// `SIGNED INTEGER` + /// Signed integer as used in [MySQL CAST] target types, with optional `INTEGER` suffix, + /// e.g. `SIGNED INTEGER` /// /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html SignedInteger, - /// Signed integer as used in [MySQL CAST] target types, without optional `INTEGER` suffix: - /// `SIGNED` + /// Signed integer as used in [MySQL CAST] target types, without optional `INTEGER` suffix, + /// e.g. `SIGNED` /// /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html Unsigned, - /// Unsigned integer as used in [MySQL CAST] target types, with optional `INTEGER` suffix: - /// `UNSIGNED INTEGER` + /// Unsigned integer as used in [MySQL CAST] target types, with optional `INTEGER` suffix, + /// e.g. `UNSIGNED INTEGER`. /// /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html UnsignedInteger, - /// Float4 as alias for Real in [postgresql] + /// Float4 is an alias for Real in [PostgreSQL]. /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Float4, - /// Floating point in [clickhouse] + /// Floating point in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float Float32, - /// Floating point in [bigquery] + /// Floating point in [BigQuery]. /// - /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#floating_point_types - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#floating_point_types + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float Float64, - /// Floating point e.g. REAL + /// Floating point, e.g. REAL. Real, - /// Float8 as alias for Double in [postgresql] + /// Float8 is an alias for Double in [PostgreSQL]. /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Float8, /// Double Double(ExactNumberInfo), - /// Double PRECISION e.g. [standard], [postgresql] + /// Double Precision, see [SQL Standard], [PostgreSQL]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#approximate-numeric-type - /// [postgresql]: https://www.postgresql.org/docs/current/datatype-numeric.html + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#approximate-numeric-type + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-numeric.html DoublePrecision, - /// Bool as alias for Boolean in [postgresql] + /// Bool is an alias for Boolean, see [PostgreSQL]. /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Bool, - /// Boolean + /// Boolean type. Boolean, - /// Date + /// Date type. Date, - /// Date32 with the same range as Datetime64 + /// Date32 with the same range as Datetime64. /// /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/date32 Date32, - /// Time with optional time precision and time zone information e.g. [standard][1]. + /// Time with optional time precision and time zone information, see [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type Time(Option, TimezoneInfo), - /// Datetime with optional time precision e.g. [MySQL][1]. + /// Datetime with optional time precision, see [MySQL][1]. /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/datetime.html Datetime(Option), - /// Datetime with time precision and optional timezone e.g. [ClickHouse][1]. + /// Datetime with time precision and optional timezone, see [ClickHouse][1]. /// /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/datetime64 Datetime64(u64, Option), - /// Timestamp with optional time precision and time zone information e.g. [standard][1]. + /// Timestamp with optional time precision and time zone information, see [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type Timestamp(Option, TimezoneInfo), @@ -316,25 +335,27 @@ pub enum DataType { /// /// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type TimestampNtz, - /// Interval + /// Interval type. Interval, - /// JSON type + /// JSON type. JSON, - /// Binary JSON type + /// Binary JSON type. JSONB, - /// Regclass used in postgresql serial + /// Regclass used in [PostgreSQL] serial. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Regclass, - /// Text + /// Text type. Text, - /// [MySQL] text with up to 2**8 bytes + /// [MySQL] text with up to 2**8 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html TinyText, - /// [MySQL] text with up to 2**24 bytes + /// [MySQL] text with up to 2**24 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html MediumText, - /// [MySQL] text with up to 2**32 bytes + /// [MySQL] text with up to 2**32 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html LongText, @@ -344,75 +365,76 @@ pub enum DataType { /// /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/fixedstring FixedString(u64), - /// Bytea - Bytea, - /// Bit string, e.g. [Postgres], [MySQL], or [MSSQL] + /// Bytea type, see [PostgreSQL]. /// - /// [Postgres]: https://www.postgresql.org/docs/current/datatype-bit.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-bit.html + Bytea, + /// Bit string, see [PostgreSQL], [MySQL], or [MSSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-bit.html /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/bit-type.html /// [MSSQL]: https://learn.microsoft.com/en-us/sql/t-sql/data-types/bit-transact-sql?view=sql-server-ver16 Bit(Option), - /// `BIT VARYING(n)`: Variable-length bit string e.g. [Postgres] + /// `BIT VARYING(n)`: Variable-length bit string, see [PostgreSQL]. /// - /// [Postgres]: https://www.postgresql.org/docs/current/datatype-bit.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-bit.html BitVarying(Option), - /// `VARBIT(n)`: Variable-length bit string. [Postgres] alias for `BIT VARYING` + /// `VARBIT(n)`: Variable-length bit string. [PostgreSQL] alias for `BIT VARYING`. /// - /// [Postgres]: https://www.postgresql.org/docs/current/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html VarBit(Option), - /// - /// Custom type such as enums + /// Custom types. Custom(ObjectName, Vec), - /// Arrays + /// Arrays. Array(ArrayElemTypeDef), - /// Map + /// Map, see [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/map + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/map Map(Box, Box), - /// Tuple + /// Tuple, see [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple Tuple(Vec), - /// Nested + /// Nested type, see [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nested-data-structures/nested + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nested-data-structures/nested Nested(Vec), - /// Enums + /// Enum type. Enum(Vec, Option), - /// Set + /// Set type. Set(Vec), - /// Struct + /// Struct type, see [Hive], [BigQuery]. /// - /// [hive]: https://docs.cloudera.com/cdw-runtime/cloud/impala-sql-reference/topics/impala-struct.html - /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type + /// [Hive]: https://docs.cloudera.com/cdw-runtime/cloud/impala-sql-reference/topics/impala-struct.html + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type Struct(Vec, StructBracketKind), - /// Union + /// Union type, see [DuckDB]. /// - /// [duckdb]: https://duckdb.org/docs/sql/data_types/union.html + /// [DuckDB]: https://duckdb.org/docs/sql/data_types/union.html Union(Vec), /// Nullable - special marker NULL represents in ClickHouse as a data type. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nullable + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nullable Nullable(Box), /// LowCardinality - changes the internal representation of other data types to be dictionary-encoded. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/lowcardinality + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/lowcardinality LowCardinality(Box), /// No type specified - only used with /// [`SQLiteDialect`](crate::dialect::SQLiteDialect), from statements such /// as `CREATE TABLE t1 (a)`. Unspecified, - /// Trigger data type, returned by functions associated with triggers + /// Trigger data type, returned by functions associated with triggers, see [PostgreSQL]. /// - /// [postgresql]: https://www.postgresql.org/docs/current/plpgsql-trigger.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/plpgsql-trigger.html Trigger, - /// Any data type, used in BigQuery UDF definitions for templated parameters + /// Any data type, used in BigQuery UDF definitions for templated parameters, see [BigQuery]. /// - /// [bigquery]: https://cloud.google.com/bigquery/docs/user-defined-functions#templated-sql-udf-parameters + /// [BigQuery]: https://cloud.google.com/bigquery/docs/user-defined-functions#templated-sql-udf-parameters AnyType, - /// geometric type + /// Geometric type, see [PostgreSQL]. /// - /// [Postgres]: https://www.postgresql.org/docs/9.5/functions-geometry.html + /// [PostgreSQL]: https://www.postgresql.org/docs/9.5/functions-geometry.html GeometricType(GeometricTypeKind), } @@ -503,6 +525,9 @@ impl fmt::Display for DataType { DataType::Int256 => { write!(f, "Int256") } + DataType::HugeInt => { + write!(f, "HUGEINT") + } DataType::Int4Unsigned(zerofill) => { format_type_with_optional_length(f, "INT4", zerofill, true) } @@ -521,6 +546,18 @@ impl fmt::Display for DataType { DataType::Int8Unsigned(zerofill) => { format_type_with_optional_length(f, "INT8", zerofill, true) } + DataType::UTinyInt => { + write!(f, "UTINYINT") + } + DataType::USmallInt => { + write!(f, "USMALLINT") + } + DataType::UBigInt => { + write!(f, "UBIGINT") + } + DataType::UHugeInt => { + write!(f, "UHUGEINT") + } DataType::UInt8 => { write!(f, "UInt8") } @@ -782,19 +819,19 @@ pub enum StructBracketKind { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TimezoneInfo { - /// No information about time zone. E.g., TIMESTAMP + /// No information about time zone, e.g. TIMESTAMP None, - /// Temporal type 'WITH TIME ZONE'. E.g., TIMESTAMP WITH TIME ZONE, [standard], [Oracle] + /// Temporal type 'WITH TIME ZONE', e.g. TIMESTAMP WITH TIME ZONE, [SQL Standard], [Oracle] /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type /// [Oracle]: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/nlspg/datetime-data-types-and-time-zone-support.html#GUID-3F1C388E-C651-43D5-ADBC-1A49E5C2CA05 WithTimeZone, - /// Temporal type 'WITHOUT TIME ZONE'. E.g., TIME WITHOUT TIME ZONE, [standard], [Postgresql] + /// Temporal type 'WITHOUT TIME ZONE', e.g. TIME WITHOUT TIME ZONE, [SQL Standard], [Postgresql] /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type /// [Postgresql]: https://www.postgresql.org/docs/current/datatype-datetime.html WithoutTimeZone, - /// Postgresql specific `WITH TIME ZONE` formatting, for both TIME and TIMESTAMP. E.g., TIMETZ, [Postgresql] + /// Postgresql specific `WITH TIME ZONE` formatting, for both TIME and TIMESTAMP, e.g. TIMETZ, [Postgresql] /// /// [Postgresql]: https://www.postgresql.org/docs/current/datatype-datetime.html Tz, @@ -823,18 +860,18 @@ impl fmt::Display for TimezoneInfo { } /// Additional information for `NUMERIC`, `DECIMAL`, and `DEC` data types -/// following the 2016 [standard]. +/// following the 2016 [SQL Standard]. /// -/// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type +/// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ExactNumberInfo { - /// No additional information e.g. `DECIMAL` + /// No additional information, e.g. `DECIMAL` None, - /// Only precision information e.g. `DECIMAL(10)` + /// Only precision information, e.g. `DECIMAL(10)` Precision(u64), - /// Precision and scale information e.g. `DECIMAL(10,2)` + /// Precision and scale information, e.g. `DECIMAL(10,2)` PrecisionAndScale(u64, u64), } @@ -888,7 +925,7 @@ impl fmt::Display for CharacterLength { } } -/// Possible units for characters, initially based on 2016 ANSI [standard][1]. +/// Possible units for characters, initially based on 2016 ANSI [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#char-length-units #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -961,7 +998,7 @@ pub enum ArrayElemTypeDef { /// Represents different types of geometric shapes which are commonly used in /// PostgreSQL/Redshift for spatial operations and geometry-related computations. /// -/// [Postgres]: https://www.postgresql.org/docs/9.5/functions-geometry.html +/// [PostgreSQL]: https://www.postgresql.org/docs/9.5/functions-geometry.html #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6a649b73..000ab3a4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1276,9 +1276,9 @@ impl fmt::Display for IndexOption { } } -/// [Postgres] unique index nulls handling option: `[ NULLS [ NOT ] DISTINCT ]` +/// [PostgreSQL] unique index nulls handling option: `[ NULLS [ NOT ] DISTINCT ]` /// -/// [Postgres]: https://www.postgresql.org/docs/17/sql-altertable.html +/// [PostgreSQL]: https://www.postgresql.org/docs/17/sql-altertable.html #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2175,15 +2175,15 @@ pub struct CreateFunction { /// /// IMMUTABLE | STABLE | VOLATILE /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) pub behavior: Option, /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) pub called_on_null: Option, /// PARALLEL { UNSAFE | RESTRICTED | SAFE } /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) pub parallel: Option, /// USING ... (Hive only) pub using: Option, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8537bd85..a6dc682d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -410,7 +410,7 @@ impl fmt::Display for Interval { /// A field definition within a struct /// -/// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type +/// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -431,7 +431,7 @@ impl fmt::Display for StructField { /// A field definition within a union /// -/// [duckdb]: https://duckdb.org/docs/sql/data_types/union.html +/// [DuckDB]: https://duckdb.org/docs/sql/data_types/union.html #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -448,7 +448,7 @@ impl fmt::Display for UnionField { /// A dictionary field within a dictionary. /// -/// [duckdb]: https://duckdb.org/docs/sql/data_types/struct#creating-structs +/// [DuckDB]: https://duckdb.org/docs/sql/data_types/struct#creating-structs #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -479,7 +479,7 @@ impl Display for Map { /// A map field within a map. /// -/// [duckdb]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps +/// [DuckDB]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2385,10 +2385,10 @@ impl fmt::Display for DeclareAssignment { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum DeclareType { - /// Cursor variable type. e.g. [Snowflake] [Postgres] + /// Cursor variable type. e.g. [Snowflake] [PostgreSQL] /// /// [Snowflake]: https://docs.snowflake.com/en/developer-guide/snowflake-scripting/cursors#declaring-a-cursor - /// [Postgres]: https://www.postgresql.org/docs/current/plpgsql-cursors.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/plpgsql-cursors.html Cursor, /// Result set variable type. [Snowflake] @@ -2427,7 +2427,7 @@ impl fmt::Display for DeclareType { } /// A `DECLARE` statement. -/// [Postgres] [Snowflake] [BigQuery] +/// [PostgreSQL] [Snowflake] [BigQuery] /// /// Examples: /// ```sql @@ -2435,7 +2435,7 @@ impl fmt::Display for DeclareType { /// DECLARE liahona CURSOR FOR SELECT * FROM films; /// ``` /// -/// [Postgres]: https://www.postgresql.org/docs/current/sql-declare.html +/// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-declare.html /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/snowflake-scripting/declare /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#declare #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -3020,7 +3020,7 @@ pub enum Statement { /// ```sql /// CREATE ROLE /// ``` - /// See [postgres](https://www.postgresql.org/docs/current/sql-createrole.html) + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrole.html) CreateRole { names: Vec, if_not_exists: bool, @@ -3046,7 +3046,7 @@ pub enum Statement { /// ```sql /// CREATE SECRET /// ``` - /// See [duckdb](https://duckdb.org/docs/sql/statements/create_secret.html) + /// See [DuckDB](https://duckdb.org/docs/sql/statements/create_secret.html) CreateSecret { or_replace: bool, temporary: Option, @@ -3550,7 +3550,7 @@ pub enum Statement { /// /// Supported variants: /// 1. [Hive](https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction) - /// 2. [Postgres](https://www.postgresql.org/docs/15/sql-createfunction.html) + /// 2. [PostgreSQL](https://www.postgresql.org/docs/15/sql-createfunction.html) /// 3. [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement) CreateFunction(CreateFunction), /// CREATE TRIGGER @@ -8281,7 +8281,7 @@ impl fmt::Display for FunctionDeterminismSpecifier { /// where within the statement, the body shows up. /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 -/// [Postgres]: https://www.postgresql.org/docs/15/sql-createfunction.html +/// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -8319,7 +8319,7 @@ pub enum CreateFunctionBody { /// RETURN a + b; /// ``` /// - /// [Postgres]: https://www.postgresql.org/docs/current/sql-createfunction.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html Return(Expr), } @@ -8625,9 +8625,9 @@ impl Display for CreateViewParams { } } -/// Engine of DB. Some warehouse has parameters of engine, e.g. [clickhouse] +/// Engine of DB. Some warehouse has parameters of engine, e.g. [ClickHouse] /// -/// [clickhouse]: https://clickhouse.com/docs/en/engines/table-engines +/// [ClickHouse]: https://clickhouse.com/docs/en/engines/table-engines #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 79184dd7..e41964f4 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -995,7 +995,7 @@ pub trait Dialect: Debug + Any { /// Returns true if the dialect supports `SET NAMES [COLLATE ]`. /// /// - [MySQL](https://dev.mysql.com/doc/refman/8.4/en/set-names.html) - /// - [Postgres](https://www.postgresql.org/docs/17/sql-set.html) + /// - [PostgreSQL](https://www.postgresql.org/docs/17/sql-set.html) /// /// Note: Postgres doesn't support the `COLLATE` clause, but we permissively parse it anyway. fn supports_set_names(&self) -> bool { diff --git a/src/keywords.rs b/src/keywords.rs index a1b4c0c3..73c24426 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -411,6 +411,7 @@ define_keywords!( HOSTS, HOUR, HOURS, + HUGEINT, ICEBERG, ID, IDENTITY, @@ -908,7 +909,9 @@ define_keywords!( TRY_CONVERT, TUPLE, TYPE, + UBIGINT, UESCAPE, + UHUGEINT, UINT128, UINT16, UINT256, @@ -942,6 +945,8 @@ define_keywords!( USER, USER_RESOURCES, USING, + USMALLINT, + UTINYINT, UUID, VACUUM, VALID, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 50a9aae8..54954997 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3779,7 +3779,7 @@ impl<'a> Parser<'a> { }) } - /// Parse a postgresql casting style which is in the form of `expr::datatype`. + /// Parse a PostgreSQL casting style which is in the form of `expr::datatype`. pub fn parse_pg_cast(&mut self, expr: Expr) -> Result { Ok(Expr::Cast { kind: CastKind::DoubleColon, @@ -4873,9 +4873,9 @@ impl<'a> Parser<'a> { } } - /// Parse `CREATE FUNCTION` for [Postgres] + /// Parse `CREATE FUNCTION` for [PostgreSQL] /// - /// [Postgres]: https://www.postgresql.org/docs/15/sql-createfunction.html + /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html fn parse_postgres_create_function( &mut self, or_replace: bool, @@ -9171,6 +9171,11 @@ impl<'a> Parser<'a> { Ok(DataType::BigInt(optional_precision?)) } } + Keyword::HUGEINT => Ok(DataType::HugeInt), + Keyword::UBIGINT => Ok(DataType::UBigInt), + Keyword::UHUGEINT => Ok(DataType::UHugeInt), + Keyword::USMALLINT => Ok(DataType::USmallInt), + Keyword::UTINYINT => Ok(DataType::UTinyInt), Keyword::UINT8 => Ok(DataType::UInt8), Keyword::UINT16 => Ok(DataType::UInt16), Keyword::UINT32 => Ok(DataType::UInt32), diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index bed02428..a421154a 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -352,6 +352,32 @@ fn test_duckdb_load_extension() { ); } +#[test] +fn test_duckdb_specific_int_types() { + let duckdb_dtypes = vec![ + ("UTINYINT", DataType::UTinyInt), + ("USMALLINT", DataType::USmallInt), + ("UBIGINT", DataType::UBigInt), + ("UHUGEINT", DataType::UHugeInt), + ("HUGEINT", DataType::HugeInt), + ]; + for (dtype_string, data_type) in duckdb_dtypes { + let sql = format!("SELECT 123::{}", dtype_string); + let select = duckdb().verified_only_select(&sql); + assert_eq!( + &Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Value( + Value::Number("123".parse().unwrap(), false).with_empty_span() + )), + data_type: data_type.clone(), + format: None, + }, + expr_from_projection(&select.projection[0]) + ); + } +} + #[test] fn test_duckdb_struct_literal() { //struct literal syntax https://duckdb.org/docs/sql/data_types/struct#creating-structs From 0d2976d723f083961d5c90ad998073c6f34d2ee0 Mon Sep 17 00:00:00 2001 From: Roman Borschel Date: Sun, 6 Apr 2025 07:09:24 +0200 Subject: [PATCH 031/130] Add support for MSSQL IF/ELSE statements. (#1791) Co-authored-by: Roman Borschel --- src/ast/mod.rs | 176 +++++++++++++++++++++++++------------- src/ast/spans.rs | 81 +++++++++++------- src/dialect/mssql.rs | 128 +++++++++++++++++++++++++++ src/parser/mod.rs | 68 +++++++++------ tests/sqlparser_common.rs | 108 +++++++++++++++++++---- tests/sqlparser_mssql.rs | 103 +++++++++++++++++++++- 6 files changed, 530 insertions(+), 134 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a6dc682d..3ee19043 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -37,7 +37,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::tokenizer::Span; +use crate::keywords::Keyword; +use crate::tokenizer::{Span, Token}; pub use self::data_type::{ ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember, @@ -2118,20 +2119,23 @@ pub enum Password { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct CaseStatement { + /// The `CASE` token that starts the statement. + pub case_token: AttachedToken, pub match_expr: Option, - pub when_blocks: Vec, - pub else_block: Option>, - /// TRUE if the statement ends with `END CASE` (vs `END`). - pub has_end_case: bool, + pub when_blocks: Vec, + pub else_block: Option, + /// The last token of the statement (`END` or `CASE`). + pub end_case_token: AttachedToken, } impl fmt::Display for CaseStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let CaseStatement { + case_token: _, match_expr, when_blocks, else_block, - has_end_case, + end_case_token: AttachedToken(end), } = self; write!(f, "CASE")?; @@ -2145,13 +2149,15 @@ impl fmt::Display for CaseStatement { } if let Some(else_block) = else_block { - write!(f, " ELSE ")?; - format_statement_list(f, else_block)?; + write!(f, " {else_block}")?; } write!(f, " END")?; - if *has_end_case { - write!(f, " CASE")?; + + if let Token::Word(w) = &end.token { + if w.keyword == Keyword::CASE { + write!(f, " CASE")?; + } } Ok(()) @@ -2160,7 +2166,7 @@ impl fmt::Display for CaseStatement { /// An `IF` statement. /// -/// Examples: +/// Example (BigQuery or Snowflake): /// ```sql /// IF TRUE THEN /// SELECT 1; @@ -2171,16 +2177,22 @@ impl fmt::Display for CaseStatement { /// SELECT 4; /// END IF /// ``` -/// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#if) /// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/if) +/// +/// Example (MSSQL): +/// ```sql +/// IF 1=1 SELECT 1 ELSE SELECT 2 +/// ``` +/// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/if-else-transact-sql?view=sql-server-ver16) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct IfStatement { - pub if_block: ConditionalStatements, - pub elseif_blocks: Vec, - pub else_block: Option>, + pub if_block: ConditionalStatementBlock, + pub elseif_blocks: Vec, + pub else_block: Option, + pub end_token: Option, } impl fmt::Display for IfStatement { @@ -2189,79 +2201,125 @@ impl fmt::Display for IfStatement { if_block, elseif_blocks, else_block, + end_token, } = self; write!(f, "{if_block}")?; - if !elseif_blocks.is_empty() { - write!(f, " {}", display_separated(elseif_blocks, " "))?; + for elseif_block in elseif_blocks { + write!(f, " {elseif_block}")?; } if let Some(else_block) = else_block { - write!(f, " ELSE ")?; - format_statement_list(f, else_block)?; + write!(f, " {else_block}")?; } - write!(f, " END IF")?; + if let Some(AttachedToken(end_token)) = end_token { + write!(f, " END {end_token}")?; + } Ok(()) } } -/// Represents a type of [ConditionalStatements] -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum ConditionalStatementKind { - /// `WHEN THEN ` - When, - /// `IF THEN ` - If, - /// `ELSEIF THEN ` - ElseIf, -} - /// A block within a [Statement::Case] or [Statement::If]-like statement /// -/// Examples: +/// Example 1: /// ```sql /// WHEN EXISTS(SELECT 1) THEN SELECT 1; +/// ``` /// +/// Example 2: +/// ```sql /// IF TRUE THEN SELECT 1; SELECT 2; /// ``` +/// +/// Example 3: +/// ```sql +/// ELSE SELECT 1; SELECT 2; +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct ConditionalStatements { - /// The condition expression. - pub condition: Expr, - /// Statement list of the `THEN` clause. - pub statements: Vec, - pub kind: ConditionalStatementKind, +pub struct ConditionalStatementBlock { + pub start_token: AttachedToken, + pub condition: Option, + pub then_token: Option, + pub conditional_statements: ConditionalStatements, +} + +impl ConditionalStatementBlock { + pub fn statements(&self) -> &Vec { + self.conditional_statements.statements() + } +} + +impl fmt::Display for ConditionalStatementBlock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let ConditionalStatementBlock { + start_token: AttachedToken(start_token), + condition, + then_token, + conditional_statements, + } = self; + + write!(f, "{start_token}")?; + + if let Some(condition) = condition { + write!(f, " {condition}")?; + } + + if then_token.is_some() { + write!(f, " THEN")?; + } + + if !conditional_statements.statements().is_empty() { + write!(f, " {conditional_statements}")?; + } + + Ok(()) + } +} + +/// A list of statements in a [ConditionalStatementBlock]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ConditionalStatements { + /// SELECT 1; SELECT 2; SELECT 3; ... + Sequence { statements: Vec }, + /// BEGIN SELECT 1; SELECT 2; SELECT 3; ... END + BeginEnd { + begin_token: AttachedToken, + statements: Vec, + end_token: AttachedToken, + }, +} + +impl ConditionalStatements { + pub fn statements(&self) -> &Vec { + match self { + ConditionalStatements::Sequence { statements } => statements, + ConditionalStatements::BeginEnd { statements, .. } => statements, + } + } } impl fmt::Display for ConditionalStatements { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let ConditionalStatements { - condition: expr, - statements, - kind, - } = self; - - let kind = match kind { - ConditionalStatementKind::When => "WHEN", - ConditionalStatementKind::If => "IF", - ConditionalStatementKind::ElseIf => "ELSEIF", - }; - - write!(f, "{kind} {expr} THEN")?; - - if !statements.is_empty() { - write!(f, " ")?; - format_statement_list(f, statements)?; + match self { + ConditionalStatements::Sequence { statements } => { + if !statements.is_empty() { + format_statement_list(f, statements)?; + } + Ok(()) + } + ConditionalStatements::BeginEnd { statements, .. } => { + write!(f, "BEGIN ")?; + format_statement_list(f, statements)?; + write!(f, " END") + } } - - Ok(()) } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 11770d1b..d253f891 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -22,21 +22,22 @@ use crate::tokenizer::Span; use super::{ dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterIndexOperation, AlterTableOperation, Array, Assignment, AssignmentTarget, CaseStatement, - CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatements, - ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, - CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, - ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, - FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, - IfStatement, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, - JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, - Query, RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, - ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, - Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, - TableFactor, TableObject, TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, - Value, Values, ViewColumnDef, WildcardAdditionalOptions, With, WithFill, + AlterIndexOperation, AlterTableOperation, Array, Assignment, AssignmentTarget, AttachedToken, + CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, + ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectBy, + ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, + Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, + Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, + FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, Insert, Interpolate, + InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, + LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, + Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, + PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, + ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, + SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, + TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, + TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -739,19 +740,14 @@ impl Spanned for CreateIndex { impl Spanned for CaseStatement { fn span(&self) -> Span { let CaseStatement { - match_expr, - when_blocks, - else_block, - has_end_case: _, + case_token: AttachedToken(start), + match_expr: _, + when_blocks: _, + else_block: _, + end_case_token: AttachedToken(end), } = self; - union_spans( - match_expr - .iter() - .map(|e| e.span()) - .chain(when_blocks.iter().map(|b| b.span())) - .chain(else_block.iter().flat_map(|e| e.iter().map(|s| s.span()))), - ) + union_spans([start.span, end.span].into_iter()) } } @@ -761,25 +757,48 @@ impl Spanned for IfStatement { if_block, elseif_blocks, else_block, + end_token, } = self; union_spans( iter::once(if_block.span()) .chain(elseif_blocks.iter().map(|b| b.span())) - .chain(else_block.iter().flat_map(|e| e.iter().map(|s| s.span()))), + .chain(else_block.as_ref().map(|b| b.span())) + .chain(end_token.as_ref().map(|AttachedToken(t)| t.span)), ) } } impl Spanned for ConditionalStatements { fn span(&self) -> Span { - let ConditionalStatements { + match self { + ConditionalStatements::Sequence { statements } => { + union_spans(statements.iter().map(|s| s.span())) + } + ConditionalStatements::BeginEnd { + begin_token: AttachedToken(start), + statements: _, + end_token: AttachedToken(end), + } => union_spans([start.span, end.span].into_iter()), + } + } +} + +impl Spanned for ConditionalStatementBlock { + fn span(&self) -> Span { + let ConditionalStatementBlock { + start_token: AttachedToken(start_token), condition, - statements, - kind: _, + then_token, + conditional_statements, } = self; - union_spans(iter::once(condition.span()).chain(statements.iter().map(|s| s.span()))) + union_spans( + iter::once(start_token.span) + .chain(condition.as_ref().map(|c| c.span())) + .chain(then_token.as_ref().map(|AttachedToken(t)| t.span)) + .chain(iter::once(conditional_statements.span())), + ) } } diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 18a963a4..d86d68a2 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -15,7 +15,16 @@ // specific language governing permissions and limitations // under the License. +use crate::ast::helpers::attached_token::AttachedToken; +use crate::ast::{ConditionalStatementBlock, ConditionalStatements, IfStatement, Statement}; use crate::dialect::Dialect; +use crate::keywords::{self, Keyword}; +use crate::parser::{Parser, ParserError}; +use crate::tokenizer::Token; +#[cfg(not(feature = "std"))] +use alloc::{vec, vec::Vec}; + +const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[Keyword::IF, Keyword::ELSE]; /// A [`Dialect`] for [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/) #[derive(Debug)] @@ -106,4 +115,123 @@ impl Dialect for MsSqlDialect { fn supports_object_name_double_dot_notation(&self) -> bool { true } + + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) && !RESERVED_FOR_COLUMN_ALIAS.contains(kw) + } + + fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.peek_keyword(Keyword::IF) { + Some(self.parse_if_stmt(parser)) + } else { + None + } + } +} + +impl MsSqlDialect { + /// ```sql + /// IF boolean_expression + /// { sql_statement | statement_block } + /// [ ELSE + /// { sql_statement | statement_block } ] + /// ``` + fn parse_if_stmt(&self, parser: &mut Parser) -> Result { + let if_token = parser.expect_keyword(Keyword::IF)?; + + let condition = parser.parse_expr()?; + + let if_block = if parser.peek_keyword(Keyword::BEGIN) { + let begin_token = parser.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(parser, Some(Keyword::END))?; + let end_token = parser.expect_keyword(Keyword::END)?; + ConditionalStatementBlock { + start_token: AttachedToken(if_token), + condition: Some(condition), + then_token: None, + conditional_statements: ConditionalStatements::BeginEnd { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + }, + } + } else { + let stmt = parser.parse_statement()?; + ConditionalStatementBlock { + start_token: AttachedToken(if_token), + condition: Some(condition), + then_token: None, + conditional_statements: ConditionalStatements::Sequence { + statements: vec![stmt], + }, + } + }; + + while let Token::SemiColon = parser.peek_token_ref().token { + parser.advance_token(); + } + + let mut else_block = None; + if parser.peek_keyword(Keyword::ELSE) { + let else_token = parser.expect_keyword(Keyword::ELSE)?; + if parser.peek_keyword(Keyword::BEGIN) { + let begin_token = parser.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(parser, Some(Keyword::END))?; + let end_token = parser.expect_keyword(Keyword::END)?; + else_block = Some(ConditionalStatementBlock { + start_token: AttachedToken(else_token), + condition: None, + then_token: None, + conditional_statements: ConditionalStatements::BeginEnd { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + }, + }); + } else { + let stmt = parser.parse_statement()?; + else_block = Some(ConditionalStatementBlock { + start_token: AttachedToken(else_token), + condition: None, + then_token: None, + conditional_statements: ConditionalStatements::Sequence { + statements: vec![stmt], + }, + }); + } + } + + Ok(Statement::If(IfStatement { + if_block, + else_block, + elseif_blocks: Vec::new(), + end_token: None, + })) + } + + /// Parse a sequence of statements, optionally separated by semicolon. + /// + /// Stops parsing when reaching EOF or the given keyword. + fn parse_statement_list( + &self, + parser: &mut Parser, + terminal_keyword: Option, + ) -> Result, ParserError> { + let mut stmts = Vec::new(); + loop { + if let Token::EOF = parser.peek_token_ref().token { + break; + } + if let Some(term) = terminal_keyword { + if parser.peek_keyword(term) { + break; + } + } + stmts.push(parser.parse_statement()?); + while let Token::SemiColon = parser.peek_token_ref().token { + parser.advance_token(); + } + } + Ok(stmts) + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 54954997..b9076bb7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -631,7 +631,7 @@ impl<'a> Parser<'a> { /// /// See [Statement::Case] pub fn parse_case_stmt(&mut self) -> Result { - self.expect_keyword_is(Keyword::CASE)?; + let case_token = self.expect_keyword(Keyword::CASE)?; let match_expr = if self.peek_keyword(Keyword::WHEN) { None @@ -641,26 +641,26 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::WHEN)?; let when_blocks = self.parse_keyword_separated(Keyword::WHEN, |parser| { - parser.parse_conditional_statements( - ConditionalStatementKind::When, - &[Keyword::WHEN, Keyword::ELSE, Keyword::END], - ) + parser.parse_conditional_statement_block(&[Keyword::WHEN, Keyword::ELSE, Keyword::END]) })?; let else_block = if self.parse_keyword(Keyword::ELSE) { - Some(self.parse_statement_list(&[Keyword::END])?) + Some(self.parse_conditional_statement_block(&[Keyword::END])?) } else { None }; - self.expect_keyword_is(Keyword::END)?; - let has_end_case = self.parse_keyword(Keyword::CASE); + let mut end_case_token = self.expect_keyword(Keyword::END)?; + if self.peek_keyword(Keyword::CASE) { + end_case_token = self.expect_keyword(Keyword::CASE)?; + } Ok(Statement::Case(CaseStatement { + case_token: AttachedToken(case_token), match_expr, when_blocks, else_block, - has_end_case, + end_case_token: AttachedToken(end_case_token), })) } @@ -669,34 +669,38 @@ impl<'a> Parser<'a> { /// See [Statement::If] pub fn parse_if_stmt(&mut self) -> Result { self.expect_keyword_is(Keyword::IF)?; - let if_block = self.parse_conditional_statements( - ConditionalStatementKind::If, - &[Keyword::ELSE, Keyword::ELSEIF, Keyword::END], - )?; + let if_block = self.parse_conditional_statement_block(&[ + Keyword::ELSE, + Keyword::ELSEIF, + Keyword::END, + ])?; let elseif_blocks = if self.parse_keyword(Keyword::ELSEIF) { self.parse_keyword_separated(Keyword::ELSEIF, |parser| { - parser.parse_conditional_statements( - ConditionalStatementKind::ElseIf, - &[Keyword::ELSEIF, Keyword::ELSE, Keyword::END], - ) + parser.parse_conditional_statement_block(&[ + Keyword::ELSEIF, + Keyword::ELSE, + Keyword::END, + ]) })? } else { vec![] }; let else_block = if self.parse_keyword(Keyword::ELSE) { - Some(self.parse_statement_list(&[Keyword::END])?) + Some(self.parse_conditional_statement_block(&[Keyword::END])?) } else { None }; - self.expect_keywords(&[Keyword::END, Keyword::IF])?; + self.expect_keyword_is(Keyword::END)?; + let end_token = self.expect_keyword(Keyword::IF)?; Ok(Statement::If(IfStatement { if_block, elseif_blocks, else_block, + end_token: Some(AttachedToken(end_token)), })) } @@ -707,19 +711,29 @@ impl<'a> Parser<'a> { /// ```sql /// IF condition THEN statement1; statement2; /// ``` - fn parse_conditional_statements( + fn parse_conditional_statement_block( &mut self, - kind: ConditionalStatementKind, terminal_keywords: &[Keyword], - ) -> Result { - let condition = self.parse_expr()?; - self.expect_keyword_is(Keyword::THEN)?; + ) -> Result { + let start_token = self.get_current_token().clone(); // self.expect_keyword(keyword)?; + let mut then_token = None; + + let condition = match &start_token.token { + Token::Word(w) if w.keyword == Keyword::ELSE => None, + _ => { + let expr = self.parse_expr()?; + then_token = Some(AttachedToken(self.expect_keyword(Keyword::THEN)?)); + Some(expr) + } + }; + let statements = self.parse_statement_list(terminal_keywords)?; - Ok(ConditionalStatements { + Ok(ConditionalStatementBlock { + start_token: AttachedToken(start_token), condition, - statements, - kind, + then_token, + conditional_statements: ConditionalStatements::Sequence { statements }, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 36533217..14716dde 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14229,9 +14229,12 @@ fn parse_case_statement() { }; assert_eq!(Some(Expr::value(number("1"))), stmt.match_expr); - assert_eq!(Expr::value(number("2")), stmt.when_blocks[0].condition); - assert_eq!(2, stmt.when_blocks[0].statements.len()); - assert_eq!(1, stmt.else_block.unwrap().len()); + assert_eq!( + Some(Expr::value(number("2"))), + stmt.when_blocks[0].condition + ); + assert_eq!(2, stmt.when_blocks[0].statements().len()); + assert_eq!(1, stmt.else_block.unwrap().statements().len()); verified_stmt(concat!( "CASE 1", @@ -14274,17 +14277,35 @@ fn parse_case_statement() { ); } +#[test] +fn test_case_statement_span() { + let sql = "CASE 1 WHEN 2 THEN SELECT 1; SELECT 2; ELSE SELECT 3; END CASE"; + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new(Location::new(1, 1), Location::new(1, sql.len() as u64 + 1)) + ); +} + #[test] fn parse_if_statement() { + let dialects = all_dialects_except(|d| d.is::()); + let sql = "IF 1 THEN SELECT 1; ELSEIF 2 THEN SELECT 2; ELSE SELECT 3; END IF"; - let Statement::If(stmt) = verified_stmt(sql) else { + let Statement::If(IfStatement { + if_block, + elseif_blocks, + else_block, + .. + }) = dialects.verified_stmt(sql) + else { unreachable!() }; - assert_eq!(Expr::value(number("1")), stmt.if_block.condition); - assert_eq!(Expr::value(number("2")), stmt.elseif_blocks[0].condition); - assert_eq!(1, stmt.else_block.unwrap().len()); + assert_eq!(Some(Expr::value(number("1"))), if_block.condition); + assert_eq!(Some(Expr::value(number("2"))), elseif_blocks[0].condition); + assert_eq!(1, else_block.unwrap().statements().len()); - verified_stmt(concat!( + dialects.verified_stmt(concat!( "IF 1 THEN", " SELECT 1;", " SELECT 2;", @@ -14300,7 +14321,7 @@ fn parse_if_statement() { " SELECT 9;", " END IF" )); - verified_stmt(concat!( + dialects.verified_stmt(concat!( "IF 1 THEN", " SELECT 1;", " SELECT 2;", @@ -14309,7 +14330,7 @@ fn parse_if_statement() { " SELECT 4;", " END IF" )); - verified_stmt(concat!( + dialects.verified_stmt(concat!( "IF 1 THEN", " SELECT 1;", " SELECT 2;", @@ -14319,22 +14340,79 @@ fn parse_if_statement() { " SELECT 4;", " END IF" )); - verified_stmt(concat!("IF 1 THEN", " SELECT 1;", " SELECT 2;", " END IF")); - verified_stmt(concat!( + dialects.verified_stmt(concat!("IF 1 THEN", " SELECT 1;", " SELECT 2;", " END IF")); + dialects.verified_stmt(concat!( "IF (1) THEN", " SELECT 1;", " SELECT 2;", " END IF" )); - verified_stmt("IF 1 THEN END IF"); - verified_stmt("IF 1 THEN SELECT 1; ELSEIF 1 THEN END IF"); + dialects.verified_stmt("IF 1 THEN END IF"); + dialects.verified_stmt("IF 1 THEN SELECT 1; ELSEIF 1 THEN END IF"); assert_eq!( ParserError::ParserError("Expected: IF, found: EOF".to_string()), - parse_sql_statements("IF 1 THEN SELECT 1; ELSEIF 1 THEN SELECT 2; END").unwrap_err() + dialects + .parse_sql_statements("IF 1 THEN SELECT 1; ELSEIF 1 THEN SELECT 2; END") + .unwrap_err() ); } +#[test] +fn test_if_statement_span() { + let sql = "IF 1=1 THEN SELECT 1; ELSEIF 1=2 THEN SELECT 2; ELSE SELECT 3; END IF"; + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new(Location::new(1, 1), Location::new(1, sql.len() as u64 + 1)) + ); +} + +#[test] +fn test_if_statement_multiline_span() { + let sql_line1 = "IF 1 = 1 THEN SELECT 1;"; + let sql_line2 = "ELSEIF 1 = 2 THEN SELECT 2;"; + let sql_line3 = "ELSE SELECT 3;"; + let sql_line4 = "END IF"; + let sql = [sql_line1, sql_line2, sql_line3, sql_line4].join("\n"); + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(&sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new( + Location::new(1, 1), + Location::new(4, sql_line4.len() as u64 + 1) + ) + ); +} + +#[test] +fn test_conditional_statement_span() { + let sql = "IF 1=1 THEN SELECT 1; ELSEIF 1=2 THEN SELECT 2; ELSE SELECT 3; END IF"; + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(sql).unwrap(); + match parser.parse_statement().unwrap() { + Statement::If(IfStatement { + if_block, + elseif_blocks, + else_block, + .. + }) => { + assert_eq!( + Span::new(Location::new(1, 1), Location::new(1, 21)), + if_block.span() + ); + assert_eq!( + Span::new(Location::new(1, 23), Location::new(1, 47)), + elseif_blocks[0].span() + ); + assert_eq!( + Span::new(Location::new(1, 49), Location::new(1, 62)), + else_block.unwrap().span() + ); + } + stmt => panic!("Unexpected statement: {:?}", stmt), + } +} + #[test] fn parse_raise_statement() { let sql = "RAISE USING MESSAGE = 42"; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 5d76fd01..bcaf527c 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -23,7 +23,7 @@ mod test_utils; use helpers::attached_token::AttachedToken; -use sqlparser::tokenizer::Span; +use sqlparser::tokenizer::{Location, Span}; use test_utils::*; use sqlparser::ast::DataType::{Int, Text, Varbinary}; @@ -31,7 +31,7 @@ use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MsSqlDialect}; -use sqlparser::parser::ParserError; +use sqlparser::parser::{Parser, ParserError}; #[test] fn parse_mssql_identifiers() { @@ -1857,6 +1857,104 @@ fn parse_mssql_set_session_value() { ms().verified_stmt("SET ANSI_NULLS, ANSI_PADDING ON"); } +#[test] +fn parse_mssql_if_else() { + // Simple statements and blocks + ms().verified_stmt("IF 1 = 1 SELECT '1'; ELSE SELECT '2';"); + ms().verified_stmt("IF 1 = 1 BEGIN SET @A = 1; END ELSE SET @A = 2;"); + ms().verified_stmt( + "IF DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday') SELECT 'Weekend'; ELSE SELECT 'Weekday';" + ); + ms().verified_stmt( + "IF (SELECT COUNT(*) FROM a.b WHERE c LIKE 'x%') > 1 SELECT 'yes'; ELSE SELECT 'No';", + ); + + // Multiple statements + let stmts = ms() + .parse_sql_statements("DECLARE @A INT; IF 1=1 BEGIN SET @A = 1 END ELSE SET @A = 2") + .unwrap(); + match &stmts[..] { + [Statement::Declare { .. }, Statement::If(stmt)] => { + assert_eq!( + stmt.to_string(), + "IF 1 = 1 BEGIN SET @A = 1; END ELSE SET @A = 2;" + ); + } + _ => panic!("Unexpected statements: {:?}", stmts), + } +} + +#[test] +fn test_mssql_if_else_span() { + let sql = "IF 1 = 1 SELECT '1' ELSE SELECT '2'"; + let mut parser = Parser::new(&MsSqlDialect {}).try_with_sql(sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new(Location::new(1, 1), Location::new(1, sql.len() as u64 + 1)) + ); +} + +#[test] +fn test_mssql_if_else_multiline_span() { + let sql_line1 = "IF 1 = 1"; + let sql_line2 = "SELECT '1'"; + let sql_line3 = "ELSE SELECT '2'"; + let sql = [sql_line1, sql_line2, sql_line3].join("\n"); + let mut parser = Parser::new(&MsSqlDialect {}).try_with_sql(&sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new( + Location::new(1, 1), + Location::new(3, sql_line3.len() as u64 + 1) + ) + ); +} + +#[test] +fn test_mssql_if_statements_span() { + // Simple statements + let mut sql = "IF 1 = 1 SELECT '1' ELSE SELECT '2'"; + let mut parser = Parser::new(&MsSqlDialect {}).try_with_sql(sql).unwrap(); + match parser.parse_statement().unwrap() { + Statement::If(IfStatement { + if_block, + else_block: Some(else_block), + .. + }) => { + assert_eq!( + if_block.span(), + Span::new(Location::new(1, 1), Location::new(1, 20)) + ); + assert_eq!( + else_block.span(), + Span::new(Location::new(1, 21), Location::new(1, 36)) + ); + } + stmt => panic!("Unexpected statement: {:?}", stmt), + } + + // Blocks + sql = "IF 1 = 1 BEGIN SET @A = 1; END ELSE BEGIN SET @A = 2 END"; + parser = Parser::new(&MsSqlDialect {}).try_with_sql(sql).unwrap(); + match parser.parse_statement().unwrap() { + Statement::If(IfStatement { + if_block, + else_block: Some(else_block), + .. + }) => { + assert_eq!( + if_block.span(), + Span::new(Location::new(1, 1), Location::new(1, 31)) + ); + assert_eq!( + else_block.span(), + Span::new(Location::new(1, 32), Location::new(1, 57)) + ); + } + stmt => panic!("Unexpected statement: {:?}", stmt), + } +} + #[test] fn parse_mssql_varbinary_max_length() { let sql = "CREATE TABLE example (var_binary_col VARBINARY(MAX))"; @@ -1918,6 +2016,7 @@ fn parse_mssql_table_identifier_with_default_schema() { fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } + fn ms_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } From cfd8951452b97de7b59afa328a739858e1da6ce3 Mon Sep 17 00:00:00 2001 From: Roman Borschel Date: Thu, 10 Apr 2025 06:59:44 +0200 Subject: [PATCH 032/130] Allow literal backslash escapes for string literals in Redshift dialect. (#1801) Co-authored-by: Roman Borschel --- src/dialect/redshift.rs | 4 ++++ tests/sqlparser_redshift.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index d90eb6e7..feccca5d 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -125,4 +125,8 @@ impl Dialect for RedshiftSqlDialect { fn allow_extract_single_quotes(&self) -> bool { true } + + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } } diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 060e3853..be2b6722 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -397,3 +397,8 @@ fn parse_extract_single_quotes() { let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table"; redshift().verified_stmt(sql); } + +#[test] +fn parse_string_literal_backslash_escape() { + redshift().one_statement_parses_to(r#"SELECT 'l\'auto'"#, "SELECT 'l''auto'"); +} From 67c3be075ef072387590c4529d8c704c8e8340aa Mon Sep 17 00:00:00 2001 From: Roman Borschel Date: Thu, 10 Apr 2025 12:26:13 +0200 Subject: [PATCH 033/130] Add support for MySQL's STRAIGHT_JOIN join operator. (#1802) Co-authored-by: Roman Borschel --- src/ast/query.rs | 7 +++++++ src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 4 ++++ tests/sqlparser_mysql.rs | 7 +++++++ 5 files changed, 20 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index 1b30dcf1..abc115a0 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2157,6 +2157,9 @@ impl fmt::Display for Join { self.relation, suffix(constraint) ), + JoinOperator::StraightJoin(constraint) => { + write!(f, " STRAIGHT_JOIN {}{}", self.relation, suffix(constraint)) + } } } } @@ -2197,6 +2200,10 @@ pub enum JoinOperator { match_condition: Expr, constraint: JoinConstraint, }, + /// STRAIGHT_JOIN (non-standard) + /// + /// See . + StraightJoin(JoinConstraint), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d253f891..9ff83b76 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2128,6 +2128,7 @@ impl Spanned for JoinOperator { } => match_condition.span().union(&constraint.span()), JoinOperator::Anti(join_constraint) => join_constraint.span(), JoinOperator::Semi(join_constraint) => join_constraint.span(), + JoinOperator::StraightJoin(join_constraint) => join_constraint.span(), } } } diff --git a/src/keywords.rs b/src/keywords.rs index 73c24426..0b947b61 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -840,6 +840,7 @@ define_keywords!( STORAGE_INTEGRATION, STORAGE_SERIALIZATION_POLICY, STORED, + STRAIGHT_JOIN, STRICT, STRING, STRUCT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b9076bb7..0ccf10d7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11826,6 +11826,10 @@ impl<'a> Parser<'a> { Keyword::OUTER => { return self.expected("LEFT, RIGHT, or FULL", self.peek_token()); } + Keyword::STRAIGHT_JOIN => { + let _ = self.next_token(); // consume STRAIGHT_JOIN + JoinOperator::StraightJoin + } _ if natural => { return self.expected("a join type after NATURAL", self.peek_token()); } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 1d4fd6a0..c60936ca 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3587,3 +3587,10 @@ fn test_variable_assignment_using_colon_equal() { _ => panic!("Unexpected statement {stmt}"), } } + +#[test] +fn parse_straight_join() { + mysql().verified_stmt( + "SELECT a.*, b.* FROM table_a AS a STRAIGHT_JOIN table_b AS b ON a.b_id = b.id", + ); +} From d090ad4ccfd6c76c87de676fae50bbaec36e752a Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:49:43 +0200 Subject: [PATCH 034/130] Snowflake COPY INTO target columns, select items and optional alias (#1805) --- src/ast/helpers/stmt_data_loading.rs | 21 +++- src/ast/mod.rs | 14 ++- src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 166 ++++++++++++++------------- tests/sqlparser_snowflake.rs | 36 ++++-- 5 files changed, 147 insertions(+), 91 deletions(-) diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index cc4fa12f..e960bb05 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -29,7 +29,7 @@ use core::fmt; use serde::{Deserialize, Serialize}; use crate::ast::helpers::key_value_options::KeyValueOptions; -use crate::ast::{Ident, ObjectName}; +use crate::ast::{Ident, ObjectName, SelectItem}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; @@ -44,6 +44,25 @@ pub struct StageParamsObject { pub credentials: KeyValueOptions, } +/// This enum enables support for both standard SQL select item expressions +/// and Snowflake-specific ones for data loading. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum StageLoadSelectItemKind { + SelectItem(SelectItem), + StageLoadSelectItem(StageLoadSelectItem), +} + +impl fmt::Display for StageLoadSelectItemKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + StageLoadSelectItemKind::SelectItem(item) => write!(f, "{item}"), + StageLoadSelectItemKind::StageLoadSelectItem(item) => write!(f, "{item}"), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3ee19043..4031936e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -23,7 +23,10 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use helpers::{attached_token::AttachedToken, stmt_data_loading::FileStagingCommand}; +use helpers::{ + attached_token::AttachedToken, + stmt_data_loading::{FileStagingCommand, StageLoadSelectItemKind}, +}; use core::ops::Deref; use core::{ @@ -92,7 +95,7 @@ pub use self::value::{ }; use crate::ast::helpers::key_value_options::KeyValueOptions; -use crate::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageParamsObject}; +use crate::ast::helpers::stmt_data_loading::StageParamsObject; #[cfg(feature = "visitor")] pub use visitor::*; @@ -2988,10 +2991,11 @@ pub enum Statement { CopyIntoSnowflake { kind: CopyIntoSnowflakeKind, into: ObjectName, + into_columns: Option>, from_obj: Option, from_obj_alias: Option, stage_params: StageParamsObject, - from_transformations: Option>, + from_transformations: Option>, from_query: Option>, files: Option>, pattern: Option, @@ -5583,6 +5587,7 @@ impl fmt::Display for Statement { Statement::CopyIntoSnowflake { kind, into, + into_columns, from_obj, from_obj_alias, stage_params, @@ -5596,6 +5601,9 @@ impl fmt::Display for Statement { partition, } => { write!(f, "COPY INTO {}", into)?; + if let Some(into_columns) = into_columns { + write!(f, " ({})", display_comma_separated(into_columns))?; + } if let Some(from_transformations) = from_transformations { // Data load with transformation if let Some(from_stage) = from_obj { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 9ff83b76..31a036b1 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -350,6 +350,7 @@ impl Spanned for Statement { } => source.span(), Statement::CopyIntoSnowflake { into: _, + into_columns: _, from_obj: _, from_obj_alias: _, stage_params: _, diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index f303f821..8b279c7c 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -20,7 +20,7 @@ use crate::alloc::string::ToString; use crate::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType, KeyValueOptions}; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ - FileStagingCommand, StageLoadSelectItem, StageParamsObject, + FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, @@ -30,7 +30,7 @@ use crate::ast::{ }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; -use crate::parser::{Parser, ParserError}; +use crate::parser::{IsOptional, Parser, ParserError}; use crate::tokenizer::{Token, Word}; #[cfg(not(feature = "std"))] use alloc::boxed::Box; @@ -722,7 +722,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { }; let mut files: Vec = vec![]; - let mut from_transformations: Option> = None; + let mut from_transformations: Option> = None; let mut from_stage_alias = None; let mut from_stage = None; let mut stage_params = StageParamsObject { @@ -744,6 +744,11 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { stage_params = parse_stage_params(parser)?; } + let into_columns = match &parser.peek_token().token { + Token::LParen => Some(parser.parse_parenthesized_column_list(IsOptional::Optional, true)?), + _ => None, + }; + parser.expect_keyword_is(Keyword::FROM)?; match parser.next_token().token { Token::LParen if kind == CopyIntoSnowflakeKind::Table => { @@ -755,15 +760,10 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { from_stage = Some(parse_snowflake_stage_name(parser)?); stage_params = parse_stage_params(parser)?; - // as - from_stage_alias = if parser.parse_keyword(Keyword::AS) { - Some(match parser.next_token().token { - Token::Word(w) => Ok(Ident::new(w.value)), - _ => parser.expected("stage alias", parser.peek_token()), - }?) - } else { - None - }; + // Parse an optional alias + from_stage_alias = parser + .maybe_parse_table_alias()? + .map(|table_alias| table_alias.name); parser.expect_token(&Token::RParen)?; } Token::LParen if kind == CopyIntoSnowflakeKind::Location => { @@ -846,6 +846,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { Ok(Statement::CopyIntoSnowflake { kind, into, + into_columns, from_obj: from_stage, from_obj_alias: from_stage_alias, stage_params, @@ -866,86 +867,93 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { fn parse_select_items_for_data_load( parser: &mut Parser, -) -> Result>, ParserError> { - // [.]$[.] [ , [.]$[.] ... ] - let mut select_items: Vec = vec![]; +) -> Result>, ParserError> { + let mut select_items: Vec = vec![]; loop { - let mut alias: Option = None; - let mut file_col_num: i32 = 0; - let mut element: Option = None; - let mut item_as: Option = None; + match parser.maybe_parse(parse_select_item_for_data_load)? { + // [.]$[.] [ , [.]$[.] ... ] + Some(item) => select_items.push(StageLoadSelectItemKind::StageLoadSelectItem(item)), + // Fallback, try to parse a standard SQL select item + None => select_items.push(StageLoadSelectItemKind::SelectItem( + parser.parse_select_item()?, + )), + } + if matches!(parser.peek_token_ref().token, Token::Comma) { + parser.advance_token(); + } else { + break; + } + } + Ok(Some(select_items)) +} - let next_token = parser.next_token(); - match next_token.token { +fn parse_select_item_for_data_load( + parser: &mut Parser, +) -> Result { + let mut alias: Option = None; + let mut file_col_num: i32 = 0; + let mut element: Option = None; + let mut item_as: Option = None; + + let next_token = parser.next_token(); + match next_token.token { + Token::Placeholder(w) => { + file_col_num = w.to_string().split_off(1).parse::().map_err(|e| { + ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}")) + })?; + Ok(()) + } + Token::Word(w) => { + alias = Some(Ident::new(w.value)); + Ok(()) + } + _ => parser.expected("alias or file_col_num", next_token), + }?; + + if alias.is_some() { + parser.expect_token(&Token::Period)?; + // now we get col_num token + let col_num_token = parser.next_token(); + match col_num_token.token { Token::Placeholder(w) => { file_col_num = w.to_string().split_off(1).parse::().map_err(|e| { ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}")) })?; Ok(()) } - Token::Word(w) => { - alias = Some(Ident::new(w.value)); - Ok(()) - } - _ => parser.expected("alias or file_col_num", next_token), + _ => parser.expected("file_col_num", col_num_token), }?; + } - if alias.is_some() { - parser.expect_token(&Token::Period)?; - // now we get col_num token - let col_num_token = parser.next_token(); - match col_num_token.token { - Token::Placeholder(w) => { - file_col_num = w.to_string().split_off(1).parse::().map_err(|e| { - ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}")) - })?; - Ok(()) - } - _ => parser.expected("file_col_num", col_num_token), - }?; + // try extracting optional element + match parser.next_token().token { + Token::Colon => { + // parse element + element = Some(Ident::new(match parser.next_token().token { + Token::Word(w) => Ok(w.value), + _ => parser.expected("file_col_num", parser.peek_token()), + }?)); } - - // try extracting optional element - match parser.next_token().token { - Token::Colon => { - // parse element - element = Some(Ident::new(match parser.next_token().token { - Token::Word(w) => Ok(w.value), - _ => parser.expected("file_col_num", parser.peek_token()), - }?)); - } - _ => { - // element not present move back - parser.prev_token(); - } - } - - // as - if parser.parse_keyword(Keyword::AS) { - item_as = Some(match parser.next_token().token { - Token::Word(w) => Ok(Ident::new(w.value)), - _ => parser.expected("column item alias", parser.peek_token()), - }?); - } - - select_items.push(StageLoadSelectItem { - alias, - file_col_num, - element, - item_as, - }); - - match parser.next_token().token { - Token::Comma => { - // continue - } - _ => { - parser.prev_token(); // need to move back - break; - } + _ => { + // element not present move back + parser.prev_token(); } } - Ok(Some(select_items)) + + // as + if parser.parse_keyword(Keyword::AS) { + item_as = Some(match parser.next_token().token { + Token::Word(w) => Ok(Ident::new(w.value)), + _ => parser.expected("column item alias", parser.peek_token()), + }?); + } + + Ok(StageLoadSelectItem { + alias, + file_col_num, + element, + item_as, + }) } fn parse_stage_params(parser: &mut Parser) -> Result { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 62e52e2d..4b669414 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -20,7 +20,7 @@ //! generic dialect is also tested (on the inputs it can handle). use sqlparser::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType}; -use sqlparser::ast::helpers::stmt_data_loading::StageLoadSelectItem; +use sqlparser::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageLoadSelectItemKind}; use sqlparser::ast::*; use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -2256,7 +2256,7 @@ fn test_copy_into_with_files_and_pattern_and_verification() { fn test_copy_into_with_transformations() { let sql = concat!( "COPY INTO my_company.emp_basic FROM ", - "(SELECT t1.$1:st AS st, $1:index, t2.$1 FROM @schema.general_finished AS T) ", + "(SELECT t1.$1:st AS st, $1:index, t2.$1, 4, '5' AS const_str FROM @schema.general_finished AS T) ", "FILES = ('file1.json', 'file2.json') ", "PATTERN = '.*employees0[1-5].csv.gz' ", "VALIDATION_MODE = RETURN_7_ROWS" @@ -2277,35 +2277,55 @@ fn test_copy_into_with_transformations() { ); assert_eq!( from_transformations.as_ref().unwrap()[0], - StageLoadSelectItem { + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { alias: Some(Ident::new("t1")), file_col_num: 1, element: Some(Ident::new("st")), item_as: Some(Ident::new("st")) - } + }) ); assert_eq!( from_transformations.as_ref().unwrap()[1], - StageLoadSelectItem { + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { alias: None, file_col_num: 1, element: Some(Ident::new("index")), item_as: None - } + }) ); assert_eq!( from_transformations.as_ref().unwrap()[2], - StageLoadSelectItem { + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { alias: Some(Ident::new("t2")), file_col_num: 1, element: None, item_as: None - } + }) + ); + assert_eq!( + from_transformations.as_ref().unwrap()[3], + StageLoadSelectItemKind::SelectItem(SelectItem::UnnamedExpr(Expr::Value( + Value::Number("4".parse().unwrap(), false).into() + ))) + ); + assert_eq!( + from_transformations.as_ref().unwrap()[4], + StageLoadSelectItemKind::SelectItem(SelectItem::ExprWithAlias { + expr: Expr::Value(Value::SingleQuotedString("5".parse().unwrap()).into()), + alias: Ident::new("const_str".to_string()) + }) ); } _ => unreachable!(), } assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + // Test optional AS keyword to denote an alias for the stage + let sql1 = concat!( + "COPY INTO my_company.emp_basic FROM ", + "(SELECT t1.$1:st AS st, $1:index, t2.$1, 4, '5' AS const_str FROM @schema.general_finished T) " + ); + snowflake().parse_sql_statements(sql1).unwrap(); } #[test] From bbc80d7537d31986821d00e2d6c342c5fe1337da Mon Sep 17 00:00:00 2001 From: Roman Borschel Date: Fri, 11 Apr 2025 20:58:43 +0200 Subject: [PATCH 035/130] Fix tokenization of qualified identifiers with numeric prefix. (#1803) Co-authored-by: Roman Borschel --- src/tokenizer.rs | 74 ++++++++++++++++++++---- tests/sqlparser_mysql.rs | 122 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 11 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index d33a7d8a..13bce0c0 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -895,7 +895,7 @@ impl<'a> Tokenizer<'a> { }; let mut location = state.location(); - while let Some(token) = self.next_token(&mut state)? { + while let Some(token) = self.next_token(&mut state, buf.last().map(|t| &t.token))? { let span = location.span_to(state.location()); buf.push(TokenWithSpan { token, span }); @@ -932,7 +932,11 @@ impl<'a> Tokenizer<'a> { } /// Get the next token or return None - fn next_token(&self, chars: &mut State) -> Result, TokenizerError> { + fn next_token( + &self, + chars: &mut State, + prev_token: Option<&Token>, + ) -> Result, TokenizerError> { match chars.peek() { Some(&ch) => match ch { ' ' => self.consume_and_return(chars, Token::Whitespace(Whitespace::Space)), @@ -1211,17 +1215,29 @@ impl<'a> Tokenizer<'a> { chars.next(); } + // If the dialect supports identifiers that start with a numeric prefix + // and we have now consumed a dot, check if the previous token was a Word. + // If so, what follows is definitely not part of a decimal number and + // we should yield the dot as a dedicated token so compound identifiers + // starting with digits can be parsed correctly. + if s == "." && self.dialect.supports_numeric_prefix() { + if let Some(Token::Word(_)) = prev_token { + return Ok(Some(Token::Period)); + } + } + + // Consume fractional digits. s += &peeking_next_take_while(chars, |ch, next_ch| { ch.is_ascii_digit() || is_number_separator(ch, next_ch) }); - // No number -> Token::Period + // No fraction -> Token::Period if s == "." { return Ok(Some(Token::Period)); } - let mut exponent_part = String::new(); // Parse exponent as number + let mut exponent_part = String::new(); if chars.peek() == Some(&'e') || chars.peek() == Some(&'E') { let mut char_clone = chars.peekable.clone(); exponent_part.push(char_clone.next().unwrap()); @@ -1250,14 +1266,23 @@ impl<'a> Tokenizer<'a> { } } - // mysql dialect supports identifiers that start with a numeric prefix, - // as long as they aren't an exponent number. - if self.dialect.supports_numeric_prefix() && exponent_part.is_empty() { - let word = - peeking_take_while(chars, |ch| self.dialect.is_identifier_part(ch)); + // If the dialect supports identifiers that start with a numeric prefix, + // we need to check if the value is in fact an identifier and must thus + // be tokenized as a word. + if self.dialect.supports_numeric_prefix() { + if exponent_part.is_empty() { + // If it is not a number with an exponent, it may be + // an identifier starting with digits. + let word = + peeking_take_while(chars, |ch| self.dialect.is_identifier_part(ch)); - if !word.is_empty() { - s += word.as_str(); + if !word.is_empty() { + s += word.as_str(); + return Ok(Some(Token::make_word(s.as_str(), None))); + } + } else if prev_token == Some(&Token::Period) { + // If the previous token was a period, thus not belonging to a number, + // the value we have is part of an identifier. return Ok(Some(Token::make_word(s.as_str(), None))); } } @@ -3960,4 +3985,31 @@ mod tests { ], ); } + + #[test] + fn test_tokenize_identifiers_numeric_prefix() { + all_dialects_where(|dialect| dialect.supports_numeric_prefix()) + .tokenizes_to("123abc", vec![Token::make_word("123abc", None)]); + + all_dialects_where(|dialect| dialect.supports_numeric_prefix()) + .tokenizes_to("12e34", vec![Token::Number("12e34".to_string(), false)]); + + all_dialects_where(|dialect| dialect.supports_numeric_prefix()).tokenizes_to( + "t.12e34", + vec![ + Token::make_word("t", None), + Token::Period, + Token::make_word("12e34", None), + ], + ); + + all_dialects_where(|dialect| dialect.supports_numeric_prefix()).tokenizes_to( + "t.1two3", + vec![ + Token::make_word("t", None), + Token::Period, + Token::make_word("1two3", None), + ], + ); + } } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index c60936ca..3a3d8f00 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1926,6 +1926,128 @@ fn parse_select_with_numeric_prefix_column_name() { } } +#[test] +fn parse_qualified_identifiers_with_numeric_prefix() { + // Case 1: Qualified column name that starts with digits. + match mysql().verified_stmt("SELECT t.15to29 FROM my_table AS t") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!(&[Ident::new("t"), Ident::new("15to29")], &parts[..]); + } + proj => panic!("Unexpected projection: {:?}", proj), + }, + body => panic!("Unexpected statement body: {:?}", body), + }, + stmt => panic!("Unexpected statement: {:?}", stmt), + } + + // Case 2: Qualified column name that starts with digits and on its own represents a number. + match mysql().verified_stmt("SELECT t.15e29 FROM my_table AS t") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!(&[Ident::new("t"), Ident::new("15e29")], &parts[..]); + } + proj => panic!("Unexpected projection: {:?}", proj), + }, + body => panic!("Unexpected statement body: {:?}", body), + }, + stmt => panic!("Unexpected statement: {:?}", stmt), + } + + // Case 3: Unqualified, the same token is parsed as a number. + match mysql() + .parse_sql_statements("SELECT 15e29 FROM my_table") + .unwrap() + .pop() + { + Some(Statement::Query(q)) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { value, .. }))) => { + assert_eq!(&number("15e29"), value); + } + proj => panic!("Unexpected projection: {:?}", proj), + }, + body => panic!("Unexpected statement body: {:?}", body), + }, + stmt => panic!("Unexpected statement: {:?}", stmt), + } + + // Case 4: Quoted simple identifier. + match mysql().verified_stmt("SELECT `15e29` FROM my_table") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::Identifier(name))) => { + assert_eq!(&Ident::with_quote('`', "15e29"), name); + } + proj => panic!("Unexpected projection: {:?}", proj), + }, + body => panic!("Unexpected statement body: {:?}", body), + }, + stmt => panic!("Unexpected statement: {:?}", stmt), + } + + // Case 5: Quoted compound identifier. + match mysql().verified_stmt("SELECT t.`15e29` FROM my_table AS t") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!( + &[Ident::new("t"), Ident::with_quote('`', "15e29")], + &parts[..] + ); + } + proj => panic!("Unexpected projection: {:?}", proj), + }, + body => panic!("Unexpected statement body: {:?}", body), + }, + stmt => panic!("Unexpected statement: {:?}", stmt), + } + + // Case 6: Multi-level compound identifiers. + match mysql().verified_stmt("SELECT 1db.1table.1column") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!( + &[ + Ident::new("1db"), + Ident::new("1table"), + Ident::new("1column") + ], + &parts[..] + ); + } + proj => panic!("Unexpected projection: {:?}", proj), + }, + body => panic!("Unexpected statement body: {:?}", body), + }, + stmt => panic!("Unexpected statement: {:?}", stmt), + } + + // Case 7: Multi-level compound quoted identifiers. + match mysql().verified_stmt("SELECT `1`.`2`.`3`") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!( + &[ + Ident::with_quote('`', "1"), + Ident::with_quote('`', "2"), + Ident::with_quote('`', "3") + ], + &parts[..] + ); + } + proj => panic!("Unexpected projection: {:?}", proj), + }, + body => panic!("Unexpected statement body: {:?}", body), + }, + stmt => panic!("Unexpected statement: {:?}", stmt), + } +} + // Don't run with bigdecimal as it fails like this on rust beta: // // 'parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column' From 896c088153ac340d18d027ea0c56cd89f794146b Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Sat, 12 Apr 2025 18:03:43 +0200 Subject: [PATCH 036/130] Add support for `INHERITS` option in `CREATE TABLE` statement (#1806) --- src/ast/dml.rs | 8 +++++++ src/ast/helpers/stmt_create_table.rs | 11 +++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 13 ++++++++-- tests/sqlparser_duckdb.rs | 1 + tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_postgres.rs | 36 ++++++++++++++++++++++++++++ 8 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index ccea7fbc..9cdb1ca8 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -182,6 +182,11 @@ pub struct CreateTable { /// BigQuery: Table options list. /// pub options: Option>, + /// Postgres `INHERITs` clause, which contains the list of tables from which + /// the new table inherits. + /// + /// + pub inherits: Option>, /// SQLite "STRICT" clause. /// if the "STRICT" table-option keyword is added to the end, after the closing ")", /// then strict typing rules apply to that table. @@ -405,6 +410,9 @@ impl Display for CreateTable { if let Some(order_by) = &self.order_by { write!(f, " ORDER BY {}", order_by)?; } + if let Some(inherits) = &self.inherits { + write!(f, " INHERITS ({})", display_comma_separated(inherits))?; + } if let Some(partition_by) = self.partition_by.as_ref() { write!(f, " PARTITION BY {partition_by}")?; } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 344e9dec..1c50cb84 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -97,6 +97,7 @@ pub struct CreateTableBuilder { pub cluster_by: Option>>, pub clustered_by: Option, pub options: Option>, + pub inherits: Option>, pub strict: bool, pub copy_grants: bool, pub enable_schema_evolution: Option, @@ -151,6 +152,7 @@ impl CreateTableBuilder { cluster_by: None, clustered_by: None, options: None, + inherits: None, strict: false, copy_grants: false, enable_schema_evolution: None, @@ -331,6 +333,11 @@ impl CreateTableBuilder { self } + pub fn inherits(mut self, inherits: Option>) -> Self { + self.inherits = inherits; + self + } + pub fn strict(mut self, strict: bool) -> Self { self.strict = strict; self @@ -451,6 +458,7 @@ impl CreateTableBuilder { cluster_by: self.cluster_by, clustered_by: self.clustered_by, options: self.options, + inherits: self.inherits, strict: self.strict, copy_grants: self.copy_grants, enable_schema_evolution: self.enable_schema_evolution, @@ -512,6 +520,7 @@ impl TryFrom for CreateTableBuilder { cluster_by, clustered_by, options, + inherits, strict, copy_grants, enable_schema_evolution, @@ -560,6 +569,7 @@ impl TryFrom for CreateTableBuilder { cluster_by, clustered_by, options, + inherits, strict, iceberg, copy_grants, @@ -591,6 +601,7 @@ pub(crate) struct CreateTableConfiguration { pub partition_by: Option>, pub cluster_by: Option>>, pub options: Option>, + pub inherits: Option>, } #[cfg(test)] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 31a036b1..23034151 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -581,6 +581,7 @@ impl Spanned for CreateTable { cluster_by: _, // todo, BigQuery specific clustered_by: _, // todo, Hive specific options: _, // todo, BigQuery specific + inherits: _, // todo, PostgreSQL specific strict: _, // bool copy_grants: _, // bool enable_schema_evolution: _, // bool diff --git a/src/keywords.rs b/src/keywords.rs index 0b947b61..fb273409 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -430,6 +430,7 @@ define_keywords!( INDEX, INDICATOR, INHERIT, + INHERITS, INITIALLY, INNER, INOUT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0ccf10d7..c4bec1d5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7050,6 +7050,7 @@ impl<'a> Parser<'a> { .partition_by(create_table_config.partition_by) .cluster_by(create_table_config.cluster_by) .options(create_table_config.options) + .inherits(create_table_config.inherits) .primary_key(primary_key) .strict(strict) .build()) @@ -7070,13 +7071,20 @@ impl<'a> Parser<'a> { } } - /// Parse configuration like partitioning, clustering information during the table creation. + /// Parse configuration like inheritance, partitioning, clustering information during the table creation. /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_2) - /// [PostgreSQL](https://www.postgresql.org/docs/current/ddl-partitioning.html) + /// [PostgreSQL Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) + /// [PostgreSQL Inheritance](https://www.postgresql.org/docs/current/ddl-inherit.html) fn parse_optional_create_table_config( &mut self, ) -> Result { + let inherits = if self.parse_keyword(Keyword::INHERITS) { + Some(self.parse_parenthesized_qualified_column_list(IsOptional::Mandatory, false)?) + } else { + None + }; + let partition_by = if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | GenericDialect) && self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { @@ -7105,6 +7113,7 @@ impl<'a> Parser<'a> { partition_by, cluster_by, options, + inherits, }) } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a421154a..32058324 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -756,6 +756,7 @@ fn test_duckdb_union_datatype() { cluster_by: Default::default(), clustered_by: Default::default(), options: Default::default(), + inherits: Default::default(), strict: Default::default(), copy_grants: Default::default(), enable_schema_evolution: Default::default(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index bcaf527c..44fd01f1 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1594,6 +1594,7 @@ fn parse_create_table_with_valid_options() { cluster_by: None, clustered_by: None, options: None, + inherits: None, strict: false, iceberg: false, copy_grants: false, @@ -1764,6 +1765,7 @@ fn parse_create_table_with_identity_column() { cluster_by: None, clustered_by: None, options: None, + inherits: None, strict: false, copy_grants: false, enable_schema_evolution: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a6d65ec7..098d4b1c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2733,6 +2733,41 @@ fn parse_create_brin() { } } +#[test] +fn parse_create_table_with_inherits() { + let single_inheritance_sql = + "CREATE TABLE child_table (child_column INT) INHERITS (public.parent_table)"; + match pg().verified_stmt(single_inheritance_sql) { + Statement::CreateTable(CreateTable { + inherits: Some(inherits), + .. + }) => { + assert_eq_vec(&["public", "parent_table"], &inherits[0].0); + } + _ => unreachable!(), + } + + let double_inheritance_sql = "CREATE TABLE child_table (child_column INT) INHERITS (public.parent_table, pg_catalog.pg_settings)"; + match pg().verified_stmt(double_inheritance_sql) { + Statement::CreateTable(CreateTable { + inherits: Some(inherits), + .. + }) => { + assert_eq_vec(&["public", "parent_table"], &inherits[0].0); + assert_eq_vec(&["pg_catalog", "pg_settings"], &inherits[1].0); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_table_with_empty_inherits_fails() { + assert!(matches!( + pg().parse_sql_statements("CREATE TABLE child_table (child_column INT) INHERITS ()"), + Err(ParserError::ParserError(_)) + )); +} + #[test] fn parse_create_index_concurrently() { let sql = "CREATE INDEX CONCURRENTLY IF NOT EXISTS my_index ON my_table(col1,col2)"; @@ -5426,6 +5461,7 @@ fn parse_trigger_related_functions() { cluster_by: None, clustered_by: None, options: None, + inherits: None, strict: false, copy_grants: false, enable_schema_evolution: None, From 6566c47593d3dc713e1f25e99adb59dff148494b Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 15 Apr 2025 01:50:50 -0400 Subject: [PATCH 037/130] Add `DROP TRIGGER` support for SQL Server (#1813) --- src/parser/mod.rs | 2 +- tests/sqlparser_mssql.rs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c4bec1d5..adf8d86d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5177,7 +5177,7 @@ impl<'a> Parser<'a> { /// DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] /// ``` pub fn parse_drop_trigger(&mut self) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) { + if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) { self.prev_token(); return self.expected("an object type after DROP", self.peek_token()); } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 44fd01f1..644589b5 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2038,3 +2038,18 @@ fn parse_mssql_merge_with_output() { OUTPUT $action, deleted.ProductID INTO dsi.temp_products"; ms_and_generic().verified_stmt(stmt); } + +#[test] +fn parse_drop_trigger() { + let sql_drop_trigger = "DROP TRIGGER emp_stamp;"; + let drop_stmt = ms().one_statement_parses_to(sql_drop_trigger, ""); + assert_eq!( + drop_stmt, + Statement::DropTrigger { + if_exists: false, + trigger_name: ObjectName::from(vec![Ident::new("emp_stamp")]), + table_name: None, + option: None, + } + ); +} From 514d2ecdaf24af52a5a5e7e5227bb1b1bd94f300 Mon Sep 17 00:00:00 2001 From: bar sela Date: Tue, 15 Apr 2025 08:57:26 +0300 Subject: [PATCH 038/130] Snowflake: support nested join without parentheses (#1799) --- src/parser/mod.rs | 32 ++- tests/sqlparser_snowflake.rs | 410 +++++++++++++++++++++++++++++++++++ 2 files changed, 440 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index adf8d86d..bb989fef 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11712,6 +11712,11 @@ impl<'a> Parser<'a> { // Note that for keywords to be properly handled here, they need to be // added to `RESERVED_FOR_TABLE_ALIAS`, otherwise they may be parsed as // a table alias. + let joins = self.parse_joins()?; + Ok(TableWithJoins { relation, joins }) + } + + fn parse_joins(&mut self) -> Result, ParserError> { let mut joins = vec![]; loop { let global = self.parse_keyword(Keyword::GLOBAL); @@ -11844,7 +11849,16 @@ impl<'a> Parser<'a> { } _ => break, }; - let relation = self.parse_table_factor()?; + let mut relation = self.parse_table_factor()?; + + if self.peek_parens_less_nested_join() { + let joins = self.parse_joins()?; + relation = TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { relation, joins }), + alias: None, + }; + } + let join_constraint = self.parse_join_constraint(natural)?; Join { relation, @@ -11854,7 +11868,21 @@ impl<'a> Parser<'a> { }; joins.push(join); } - Ok(TableWithJoins { relation, joins }) + Ok(joins) + } + + fn peek_parens_less_nested_join(&self) -> bool { + matches!( + self.peek_token_ref().token, + Token::Word(Word { + keyword: Keyword::JOIN + | Keyword::INNER + | Keyword::LEFT + | Keyword::RIGHT + | Keyword::FULL, + .. + }) + ) } /// A table name or a parenthesized subquery, followed by optional `[AS] alias` diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 4b669414..84c08874 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3573,3 +3573,413 @@ fn test_alter_session_followed_by_statement() { _ => panic!("Unexpected statements: {:?}", stmts), } } + +#[test] +fn test_nested_join_without_parentheses() { + let query = "SELECT DISTINCT p.product_id FROM orders AS o INNER JOIN customers AS c INNER JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o INNER JOIN (customers AS c INNER JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + })), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); + + let query = "SELECT DISTINCT p.product_id FROM orders AS o JOIN customers AS c JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o JOIN (customers AS c JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + })), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); + + let query = "SELECT DISTINCT p.product_id FROM orders AS o LEFT JOIN customers AS c LEFT JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o LEFT JOIN (customers AS c LEFT JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::Left(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + })), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::Left(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); + + let query = "SELECT DISTINCT p.product_id FROM orders AS o RIGHT JOIN customers AS c RIGHT JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o RIGHT JOIN (customers AS c RIGHT JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::Right(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + })), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::Right(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); + + let query = "SELECT DISTINCT p.product_id FROM orders AS o FULL JOIN customers AS c FULL JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o FULL JOIN (customers AS c FULL JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::FullOuter(JoinConstraint::On( + Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + } + )), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::FullOuter(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); +} From 3ad13af56318479aad6a6542594e0cd4e9040a6e Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 15 Apr 2025 18:01:18 +0100 Subject: [PATCH 039/130] Add support for parenthesized subquery as `IN` predicate (#1793) --- src/ast/mod.rs | 2 +- src/parser/mod.rs | 14 ++++++-------- tests/sqlparser_common.rs | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4031936e..72fb9997 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -728,7 +728,7 @@ pub enum Expr { /// `[ NOT ] IN (SELECT ...)` InSubquery { expr: Box, - subquery: Box, + subquery: Box, negated: bool, }, /// `[ NOT ] IN UNNEST(array_expression)` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bb989fef..d43e5d02 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3756,15 +3756,13 @@ impl<'a> Parser<'a> { }); } self.expect_token(&Token::LParen)?; - let in_op = if self.parse_keyword(Keyword::SELECT) || self.parse_keyword(Keyword::WITH) { - self.prev_token(); - Expr::InSubquery { + let in_op = match self.maybe_parse(|p| p.parse_query_body(p.dialect.prec_unknown()))? { + Some(subquery) => Expr::InSubquery { expr: Box::new(expr), - subquery: self.parse_query()?, + subquery, negated, - } - } else { - Expr::InList { + }, + None => Expr::InList { expr: Box::new(expr), list: if self.dialect.supports_in_empty_list() { self.parse_comma_separated0(Parser::parse_expr, Token::RParen)? @@ -3772,7 +3770,7 @@ impl<'a> Parser<'a> { self.parse_comma_separated(Parser::parse_expr)? }, negated, - } + }, }; self.expect_token(&Token::RParen)?; Ok(in_op) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 14716dde..be848a60 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2224,7 +2224,21 @@ fn parse_in_subquery() { assert_eq!( Expr::InSubquery { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: Box::new(verified_query("SELECT segm FROM bar")), + subquery: verified_query("SELECT segm FROM bar").body, + negated: false, + }, + select.selection.unwrap() + ); +} + +#[test] +fn parse_in_union() { + let sql = "SELECT * FROM customers WHERE segment IN ((SELECT segm FROM bar) UNION (SELECT segm FROM bar2))"; + let select = verified_only_select(sql); + assert_eq!( + Expr::InSubquery { + expr: Box::new(Expr::Identifier(Ident::new("segment"))), + subquery: verified_query("(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)").body, negated: false, }, select.selection.unwrap() From 81d8909e006f90c9b2f06fdeb4bab22935bec827 Mon Sep 17 00:00:00 2001 From: Bruno Clemente Date: Tue, 15 Apr 2025 14:05:20 -0300 Subject: [PATCH 040/130] Fix `STRAIGHT_JOIN` constraint when table alias is absent (#1812) Co-authored-by: Ifeanyi Ubah --- src/dialect/mysql.rs | 7 ++++++- tests/sqlparser_mysql.rs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 2077ea19..f69e4243 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -27,7 +27,12 @@ use crate::{ use super::keywords; -const RESERVED_FOR_TABLE_ALIAS_MYSQL: &[Keyword] = &[Keyword::USE, Keyword::IGNORE, Keyword::FORCE]; +const RESERVED_FOR_TABLE_ALIAS_MYSQL: &[Keyword] = &[ + Keyword::USE, + Keyword::IGNORE, + Keyword::FORCE, + Keyword::STRAIGHT_JOIN, +]; /// A [`Dialect`] for [MySQL](https://www.mysql.com/) #[derive(Debug)] diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 3a3d8f00..9d8d12b5 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3715,4 +3715,7 @@ fn parse_straight_join() { mysql().verified_stmt( "SELECT a.*, b.* FROM table_a AS a STRAIGHT_JOIN table_b AS b ON a.b_id = b.id", ); + // Without table alias + mysql() + .verified_stmt("SELECT a.*, b.* FROM table_a STRAIGHT_JOIN table_b AS b ON a.b_id = b.id"); } From 4a487290ce3c5233c25913bb28bf02ddab3b0fdb Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 18 Apr 2025 02:59:39 -0400 Subject: [PATCH 041/130] Add support for `PRINT` statement for SQL Server (#1811) --- src/ast/mod.rs | 21 ++++++++++++++++++++- src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 8 ++++++++ tests/sqlparser_mssql.rs | 17 +++++++++++++++++ 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 72fb9997..ab3be35c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4054,6 +4054,12 @@ pub enum Statement { arguments: Vec, options: Vec, }, + /// ```sql + /// PRINT msg_str | @local_variable | string_expr + /// ``` + /// + /// See: + Print(PrintStatement), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -5745,7 +5751,7 @@ impl fmt::Display for Statement { } Ok(()) } - + Statement::Print(s) => write!(f, "{s}"), Statement::List(command) => write!(f, "LIST {command}"), Statement::Remove(command) => write!(f, "REMOVE {command}"), } @@ -9211,6 +9217,19 @@ pub enum CopyIntoSnowflakeKind { Location, } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct PrintStatement { + pub message: Box, +} + +impl fmt::Display for PrintStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PRINT {}", self.message) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 23034151..a241fdf4 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -519,6 +519,7 @@ impl Spanned for Statement { Statement::UNLISTEN { .. } => Span::empty(), Statement::RenameTable { .. } => Span::empty(), Statement::RaisError { .. } => Span::empty(), + Statement::Print { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), } } diff --git a/src/keywords.rs b/src/keywords.rs index fb273409..a5400a5b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -686,6 +686,7 @@ define_keywords!( PRESERVE, PREWHERE, PRIMARY, + PRINT, PRIOR, PRIVILEGES, PROCEDURE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d43e5d02..a9ddd183 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -617,6 +617,7 @@ impl<'a> Parser<'a> { } // `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(), + Keyword::PRINT => self.parse_print(), _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -15056,6 +15057,13 @@ impl<'a> Parser<'a> { } } + /// Parse [Statement::Print] + fn parse_print(&mut self) -> Result { + Ok(Statement::Print(PrintStatement { + message: Box::new(self.parse_expr()?), + })) + } + /// Consume the parser and return its underlying token buffer pub fn into_tokens(self) -> Vec { self.tokens diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 644589b5..2786384b 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2053,3 +2053,20 @@ fn parse_drop_trigger() { } ); } + +#[test] +fn parse_print() { + let print_string_literal = "PRINT 'Hello, world!'"; + let print_stmt = ms().verified_stmt(print_string_literal); + assert_eq!( + print_stmt, + Statement::Print(PrintStatement { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("Hello, world!".to_string())).with_empty_span() + )), + }) + ); + + let _ = ms().verified_stmt("PRINT N'Hello, ⛄️!'"); + let _ = ms().verified_stmt("PRINT @my_variable"); +} From 3ec80e187d163c4f90c5bfc7c04ef71a2705a631 Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Sat, 19 Apr 2025 19:14:45 +0800 Subject: [PATCH 042/130] enable `supports_filter_during_aggregation` for Generic dialect (#1815) --- src/dialect/generic.rs | 4 ++++ tests/sqlparser_common.rs | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 92cfca8f..8f57e487 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -163,4 +163,8 @@ impl Dialect for GenericDialect { fn supports_comma_separated_set_assignments(&self) -> bool { true } + + fn supports_filter_during_aggregation(&self) -> bool { + true + } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index be848a60..1c03a0fa 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11642,6 +11642,20 @@ fn parse_connect_by() { #[test] fn test_selective_aggregation() { + let testing_dialects = all_dialects_where(|d| d.supports_filter_during_aggregation()); + let expected_dialects: Vec> = vec![ + Box::new(PostgreSqlDialect {}), + Box::new(DatabricksDialect {}), + Box::new(HiveDialect {}), + Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), + Box::new(GenericDialect {}), + ]; + assert_eq!(testing_dialects.dialects.len(), expected_dialects.len()); + expected_dialects + .into_iter() + .for_each(|d| assert!(d.supports_filter_during_aggregation())); + let sql = concat!( "SELECT ", "ARRAY_AGG(name) FILTER (WHERE name IS NOT NULL), ", @@ -11649,9 +11663,7 @@ fn test_selective_aggregation() { "FROM region" ); assert_eq!( - all_dialects_where(|d| d.supports_filter_during_aggregation()) - .verified_only_select(sql) - .projection, + testing_dialects.verified_only_select(sql).projection, vec![ SelectItem::UnnamedExpr(Expr::Function(Function { name: ObjectName::from(vec![Ident::new("ARRAY_AGG")]), From 945f8e0534401556657d35bfcb9a3370ddcc9b7c Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Wed, 23 Apr 2025 18:03:06 +0200 Subject: [PATCH 043/130] Add support for `XMLTABLE` (#1817) --- src/ast/mod.rs | 3 +- src/ast/query.rs | 186 ++++++++++++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 3 + src/parser/mod.rs | 97 ++++++++++++++++++++ tests/sqlparser_common.rs | 38 ++++++++ 6 files changed, 327 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ab3be35c..74e8cb55 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -81,7 +81,8 @@ pub use self::query::{ TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, - WildcardAdditionalOptions, With, WithFill, + WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, XmlPassingArgument, + XmlPassingClause, XmlTableColumn, XmlTableColumnOption, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index abc115a0..982985ec 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1271,6 +1271,37 @@ pub enum TableFactor { symbols: Vec, alias: Option, }, + /// The `XMLTABLE` table-valued function. + /// Part of the SQL standard, supported by PostgreSQL, Oracle, and DB2. + /// + /// + /// + /// ```sql + /// SELECT xmltable.* + /// FROM xmldata, + /// XMLTABLE('//ROWS/ROW' + /// PASSING data + /// COLUMNS id int PATH '@id', + /// ordinality FOR ORDINALITY, + /// "COUNTRY_NAME" text, + /// country_id text PATH 'COUNTRY_ID', + /// size_sq_km float PATH 'SIZE[@unit = "sq_km"]', + /// size_other text PATH 'concat(SIZE[@unit!="sq_km"], " ", SIZE[@unit!="sq_km"]/@unit)', + /// premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified' + /// ); + /// ```` + XmlTable { + /// Optional XMLNAMESPACES clause (empty if not present) + namespaces: Vec, + /// The row-generating XPath expression. + row_expression: Expr, + /// The PASSING clause specifying the document expression. + passing: XmlPassingClause, + /// The columns to be extracted from each generated row. + columns: Vec, + /// The alias for the table. + alias: Option, + }, } /// The table sample modifier options @@ -1936,6 +1967,31 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::XmlTable { + row_expression, + passing, + columns, + alias, + namespaces, + } => { + write!(f, "XMLTABLE(")?; + if !namespaces.is_empty() { + write!( + f, + "XMLNAMESPACES({}), ", + display_comma_separated(namespaces) + )?; + } + write!( + f, + "{row_expression}{passing} COLUMNS {columns})", + columns = display_comma_separated(columns) + )?; + if let Some(alias) = alias { + write!(f, " AS {alias}")?; + } + Ok(()) + } } } } @@ -3082,3 +3138,133 @@ pub enum UpdateTableFromKind { /// For Example: `UPDATE SET t1.name='aaa' FROM t1` AfterSet(Vec), } + +/// Defines the options for an XmlTable column: Named or ForOrdinality +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum XmlTableColumnOption { + /// A named column with a type, optional path, and default value. + NamedInfo { + /// The type of the column to be extracted. + r#type: DataType, + /// The path to the column to be extracted. If None, defaults to the column name. + path: Option, + /// Default value if path does not match + default: Option, + /// Whether the column is nullable (NULL=true, NOT NULL=false) + nullable: bool, + }, + /// The FOR ORDINALITY marker + ForOrdinality, +} + +/// A single column definition in XMLTABLE +/// +/// ```sql +/// COLUMNS +/// id int PATH '@id', +/// ordinality FOR ORDINALITY, +/// "COUNTRY_NAME" text, +/// country_id text PATH 'COUNTRY_ID', +/// size_sq_km float PATH 'SIZE[@unit = "sq_km"]', +/// size_other text PATH 'concat(SIZE[@unit!="sq_km"], " ", SIZE[@unit!="sq_km"]/@unit)', +/// premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified' +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct XmlTableColumn { + /// The name of the column. + pub name: Ident, + /// Column options: type/path/default or FOR ORDINALITY + pub option: XmlTableColumnOption, +} + +impl fmt::Display for XmlTableColumn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + match &self.option { + XmlTableColumnOption::NamedInfo { + r#type, + path, + default, + nullable, + } => { + write!(f, " {}", r#type)?; + if let Some(p) = path { + write!(f, " PATH {}", p)?; + } + if let Some(d) = default { + write!(f, " DEFAULT {}", d)?; + } + if !*nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } + XmlTableColumnOption::ForOrdinality => { + write!(f, " FOR ORDINALITY") + } + } + } +} + +/// Argument passed in the XMLTABLE PASSING clause +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct XmlPassingArgument { + pub expr: Expr, + pub alias: Option, + pub by_value: bool, // True if BY VALUE is specified +} + +impl fmt::Display for XmlPassingArgument { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.by_value { + write!(f, "BY VALUE ")?; + } + write!(f, "{}", self.expr)?; + if let Some(alias) = &self.alias { + write!(f, " AS {}", alias)?; + } + Ok(()) + } +} + +/// The PASSING clause for XMLTABLE +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct XmlPassingClause { + pub arguments: Vec, +} + +impl fmt::Display for XmlPassingClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.arguments.is_empty() { + write!(f, " PASSING {}", display_comma_separated(&self.arguments))?; + } + Ok(()) + } +} + +/// Represents a single XML namespace definition in the XMLNAMESPACES clause. +/// +/// `namespace_uri AS namespace_name` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct XmlNamespaceDefinition { + /// The namespace URI (a text expression). + pub uri: Expr, + /// The alias for the namespace (a simple identifier). + pub name: Ident, +} + +impl fmt::Display for XmlNamespaceDefinition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} AS {}", self.uri, self.name) + } +} diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a241fdf4..27d52c26 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1909,6 +1909,7 @@ impl Spanned for TableFactor { .chain(alias.as_ref().map(|alias| alias.span())), ), TableFactor::JsonTable { .. } => Span::empty(), + TableFactor::XmlTable { .. } => Span::empty(), TableFactor::Pivot { table, aggregate_functions, diff --git a/src/keywords.rs b/src/keywords.rs index a5400a5b..4eaad7ed 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -654,6 +654,7 @@ define_keywords!( PARTITION, PARTITIONED, PARTITIONS, + PASSING, PASSWORD, PAST, PATH, @@ -989,6 +990,8 @@ define_keywords!( WORK, WRITE, XML, + XMLNAMESPACES, + XMLTABLE, XOR, YEAR, YEARS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a9ddd183..77466b97 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11992,6 +11992,7 @@ impl<'a> Parser<'a> { | TableFactor::Function { alias, .. } | TableFactor::UNNEST { alias, .. } | TableFactor::JsonTable { alias, .. } + | TableFactor::XmlTable { alias, .. } | TableFactor::OpenJsonTable { alias, .. } | TableFactor::TableFunction { alias, .. } | TableFactor::Pivot { alias, .. } @@ -12107,6 +12108,9 @@ impl<'a> Parser<'a> { } else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[Token::LParen]) { self.prev_token(); self.parse_open_json_table_factor() + } else if self.parse_keyword_with_tokens(Keyword::XMLTABLE, &[Token::LParen]) { + self.prev_token(); + self.parse_xml_table_factor() } else { let name = self.parse_object_name(true)?; @@ -12339,6 +12343,99 @@ impl<'a> Parser<'a> { }) } + fn parse_xml_table_factor(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let namespaces = if self.parse_keyword(Keyword::XMLNAMESPACES) { + self.expect_token(&Token::LParen)?; + let namespaces = self.parse_comma_separated(Parser::parse_xml_namespace_definition)?; + self.expect_token(&Token::RParen)?; + self.expect_token(&Token::Comma)?; + namespaces + } else { + vec![] + }; + let row_expression = self.parse_expr()?; + let passing = self.parse_xml_passing_clause()?; + self.expect_keyword_is(Keyword::COLUMNS)?; + let columns = self.parse_comma_separated(Parser::parse_xml_table_column)?; + self.expect_token(&Token::RParen)?; + let alias = self.maybe_parse_table_alias()?; + Ok(TableFactor::XmlTable { + namespaces, + row_expression, + passing, + columns, + alias, + }) + } + + fn parse_xml_namespace_definition(&mut self) -> Result { + let uri = self.parse_expr()?; + self.expect_keyword_is(Keyword::AS)?; + let name = self.parse_identifier()?; + Ok(XmlNamespaceDefinition { uri, name }) + } + + fn parse_xml_table_column(&mut self) -> Result { + let name = self.parse_identifier()?; + + let option = if self.parse_keyword(Keyword::FOR) { + self.expect_keyword(Keyword::ORDINALITY)?; + XmlTableColumnOption::ForOrdinality + } else { + let r#type = self.parse_data_type()?; + let mut path = None; + let mut default = None; + + if self.parse_keyword(Keyword::PATH) { + path = Some(self.parse_expr()?); + } + + if self.parse_keyword(Keyword::DEFAULT) { + default = Some(self.parse_expr()?); + } + + let not_null = self.parse_keywords(&[Keyword::NOT, Keyword::NULL]); + if !not_null { + // NULL is the default but can be specified explicitly + let _ = self.parse_keyword(Keyword::NULL); + } + + XmlTableColumnOption::NamedInfo { + r#type, + path, + default, + nullable: !not_null, + } + }; + Ok(XmlTableColumn { name, option }) + } + + fn parse_xml_passing_clause(&mut self) -> Result { + let mut arguments = vec![]; + if self.parse_keyword(Keyword::PASSING) { + loop { + let by_value = + self.parse_keyword(Keyword::BY) && self.expect_keyword(Keyword::VALUE).is_ok(); + let expr = self.parse_expr()?; + let alias = if self.parse_keyword(Keyword::AS) { + Some(self.parse_identifier()?) + } else { + None + }; + arguments.push(XmlPassingArgument { + expr, + alias, + by_value, + }); + if !self.consume_token(&Token::Comma) { + break; + } + } + } + Ok(XmlPassingClause { arguments }) + } + fn parse_match_recognize(&mut self, table: TableFactor) -> Result { self.expect_token(&Token::LParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1c03a0fa..466b65ec 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11741,6 +11741,44 @@ fn test_group_by_grouping_sets() { ); } +#[test] +fn test_xmltable() { + all_dialects() + .verified_only_select("SELECT * FROM XMLTABLE('/root' PASSING data COLUMNS element TEXT)"); + + // Minimal meaningful working example: returns a single row with a single column named y containing the value z + all_dialects().verified_only_select( + "SELECT y FROM XMLTABLE('/X' PASSING 'z' COLUMNS y TEXT)", + ); + + // Test using subqueries + all_dialects().verified_only_select("SELECT y FROM XMLTABLE((SELECT '/X') PASSING (SELECT CAST('z' AS xml)) COLUMNS y TEXT PATH (SELECT 'y'))"); + + // NOT NULL + all_dialects().verified_only_select( + "SELECT y FROM XMLTABLE('/X' PASSING '' COLUMNS y TEXT NOT NULL)", + ); + + all_dialects().verified_only_select("SELECT * FROM XMLTABLE('/root/row' PASSING xmldata COLUMNS id INT PATH '@id', name TEXT PATH 'name/text()', value FLOAT PATH 'value')"); + + all_dialects().verified_only_select("SELECT * FROM XMLTABLE('//ROWS/ROW' PASSING data COLUMNS row_num FOR ORDINALITY, id INT PATH '@id', name TEXT PATH 'NAME' DEFAULT 'unnamed')"); + + // Example from https://www.postgresql.org/docs/15/functions-xml.html#FUNCTIONS-XML-PROCESSING + all_dialects().verified_only_select( + "SELECT xmltable.* FROM xmldata, XMLTABLE('//ROWS/ROW' PASSING data COLUMNS id INT PATH '@id', ordinality FOR ORDINALITY, \"COUNTRY_NAME\" TEXT, country_id TEXT PATH 'COUNTRY_ID', size_sq_km FLOAT PATH 'SIZE[@unit = \"sq_km\"]', size_other TEXT PATH 'concat(SIZE[@unit!=\"sq_km\"], \" \", SIZE[@unit!=\"sq_km\"]/@unit)', premier_name TEXT PATH 'PREMIER_NAME' DEFAULT 'not specified')" + ); + + // Example from DB2 docs without explicit PASSING clause: https://www.ibm.com/docs/en/db2/12.1.0?topic=xquery-simple-column-name-passing-xmlexists-xmlquery-xmltable + all_dialects().verified_only_select( + "SELECT X.* FROM T1, XMLTABLE('$CUSTLIST/customers/customerinfo' COLUMNS \"Cid\" BIGINT PATH '@Cid', \"Info\" XML PATH 'document{.}', \"History\" XML PATH 'NULL') AS X" + ); + + // Example from PostgreSQL with XMLNAMESPACES + all_dialects().verified_only_select( + "SELECT xmltable.* FROM XMLTABLE(XMLNAMESPACES('http://example.com/myns' AS x, 'http://example.com/b' AS \"B\"), '/x:example/x:item' PASSING (SELECT data FROM xmldata) COLUMNS foo INT PATH '@foo', bar INT PATH '@B:bar')" + ); +} + #[test] fn test_match_recognize() { use MatchRecognizePattern::*; From 2eb1e7bdd45cb8a0e9fd8d9b1a2717fa86794332 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Wed, 23 Apr 2025 12:10:57 -0400 Subject: [PATCH 044/130] Add `CREATE FUNCTION` support for SQL Server (#1808) --- src/ast/ddl.rs | 10 +++- src/ast/mod.rs | 100 ++++++++++++++++++++++++++++---- src/ast/spans.rs | 26 ++++++--- src/dialect/mssql.rs | 16 ++++-- src/parser/mod.rs | 110 ++++++++++++++++++++++++++++-------- tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_common.rs | 8 +++ tests/sqlparser_hive.rs | 4 +- tests/sqlparser_mssql.rs | 86 ++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 2 + 10 files changed, 313 insertions(+), 50 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 000ab3a4..c1c113b3 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2157,6 +2157,10 @@ impl fmt::Display for ClusteredBy { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct CreateFunction { + /// True if this is a `CREATE OR ALTER FUNCTION` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#or-alter) + pub or_alter: bool, pub or_replace: bool, pub temporary: bool, pub if_not_exists: bool, @@ -2219,9 +2223,10 @@ impl fmt::Display for CreateFunction { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "CREATE {or_replace}{temp}FUNCTION {if_not_exists}{name}", + "CREATE {or_alter}{or_replace}{temp}FUNCTION {if_not_exists}{name}", name = self.name, temp = if self.temporary { "TEMPORARY " } else { "" }, + or_alter = if self.or_alter { "OR ALTER " } else { "" }, or_replace = if self.or_replace { "OR REPLACE " } else { "" }, if_not_exists = if self.if_not_exists { "IF NOT EXISTS " @@ -2272,6 +2277,9 @@ impl fmt::Display for CreateFunction { if let Some(CreateFunctionBody::AsAfterOptions(function_body)) = &self.function_body { write!(f, " AS {function_body}")?; } + if let Some(CreateFunctionBody::AsBeginEnd(bes)) = &self.function_body { + write!(f, " AS {bes}")?; + } Ok(()) } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 74e8cb55..c4bb3fa1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2293,18 +2293,14 @@ pub enum ConditionalStatements { /// SELECT 1; SELECT 2; SELECT 3; ... Sequence { statements: Vec }, /// BEGIN SELECT 1; SELECT 2; SELECT 3; ... END - BeginEnd { - begin_token: AttachedToken, - statements: Vec, - end_token: AttachedToken, - }, + BeginEnd(BeginEndStatements), } impl ConditionalStatements { pub fn statements(&self) -> &Vec { match self { ConditionalStatements::Sequence { statements } => statements, - ConditionalStatements::BeginEnd { statements, .. } => statements, + ConditionalStatements::BeginEnd(bes) => &bes.statements, } } } @@ -2318,15 +2314,44 @@ impl fmt::Display for ConditionalStatements { } Ok(()) } - ConditionalStatements::BeginEnd { statements, .. } => { - write!(f, "BEGIN ")?; - format_statement_list(f, statements)?; - write!(f, " END") - } + ConditionalStatements::BeginEnd(bes) => write!(f, "{}", bes), } } } +/// Represents a list of statements enclosed within `BEGIN` and `END` keywords. +/// Example: +/// ```sql +/// BEGIN +/// SELECT 1; +/// SELECT 2; +/// END +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct BeginEndStatements { + pub begin_token: AttachedToken, + pub statements: Vec, + pub end_token: AttachedToken, +} + +impl fmt::Display for BeginEndStatements { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + } = self; + + write!(f, "{begin_token} ")?; + if !statements.is_empty() { + format_statement_list(f, statements)?; + } + write!(f, " {end_token}") + } +} + /// A `RAISE` statement. /// /// Examples: @@ -3615,6 +3640,7 @@ pub enum Statement { /// 1. [Hive](https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction) /// 2. [PostgreSQL](https://www.postgresql.org/docs/15/sql-createfunction.html) /// 3. [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement) + /// 4. [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql) CreateFunction(CreateFunction), /// CREATE TRIGGER /// @@ -4061,6 +4087,12 @@ pub enum Statement { /// /// See: Print(PrintStatement), + /// ```sql + /// RETURN [ expression ] + /// ``` + /// + /// See [ReturnStatement] + Return(ReturnStatement), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -5753,6 +5785,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::Print(s) => write!(f, "{s}"), + Statement::Return(r) => write!(f, "{r}"), Statement::List(command) => write!(f, "LIST {command}"), Statement::Remove(command) => write!(f, "REMOVE {command}"), } @@ -8355,6 +8388,7 @@ impl fmt::Display for FunctionDeterminismSpecifier { /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html +/// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -8383,6 +8417,22 @@ pub enum CreateFunctionBody { /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 AsAfterOptions(Expr), + /// Function body with statements before the `RETURN` keyword. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION my_scalar_udf(a INT, b INT) + /// RETURNS INT + /// AS + /// BEGIN + /// DECLARE c INT; + /// SET c = a + b; + /// RETURN c; + /// END + /// ``` + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql + AsBeginEnd(BeginEndStatements), /// Function body expression using the 'RETURN' keyword. /// /// Example: @@ -9231,6 +9281,34 @@ impl fmt::Display for PrintStatement { } } +/// Represents a `Return` statement. +/// +/// [MsSql triggers](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql) +/// [MsSql functions](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ReturnStatement { + pub value: Option, +} + +impl fmt::Display for ReturnStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.value { + Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {}", expr), + None => write!(f, "RETURN"), + } + } +} + +/// Variants of a `RETURN` statement +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ReturnStatementValue { + Expr(Expr), +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 27d52c26..16ff660d 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -23,8 +23,8 @@ use crate::tokenizer::Span; use super::{ dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, AlterIndexOperation, AlterTableOperation, Array, Assignment, AssignmentTarget, AttachedToken, - CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, - ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectBy, + BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, + ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, @@ -520,6 +520,7 @@ impl Spanned for Statement { Statement::RenameTable { .. } => Span::empty(), Statement::RaisError { .. } => Span::empty(), Statement::Print { .. } => Span::empty(), + Statement::Return { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), } } @@ -778,11 +779,7 @@ impl Spanned for ConditionalStatements { ConditionalStatements::Sequence { statements } => { union_spans(statements.iter().map(|s| s.span())) } - ConditionalStatements::BeginEnd { - begin_token: AttachedToken(start), - statements: _, - end_token: AttachedToken(end), - } => union_spans([start.span, end.span].into_iter()), + ConditionalStatements::BeginEnd(bes) => bes.span(), } } } @@ -2282,6 +2279,21 @@ impl Spanned for TableObject { } } +impl Spanned for BeginEndStatements { + fn span(&self) -> Span { + let BeginEndStatements { + begin_token, + statements, + end_token, + } = self; + union_spans( + core::iter::once(begin_token.0.span) + .chain(statements.iter().map(|i| i.span())) + .chain(core::iter::once(end_token.0.span)), + ) + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index d86d68a2..31e324f0 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -16,7 +16,9 @@ // under the License. use crate::ast::helpers::attached_token::AttachedToken; -use crate::ast::{ConditionalStatementBlock, ConditionalStatements, IfStatement, Statement}; +use crate::ast::{ + BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, IfStatement, Statement, +}; use crate::dialect::Dialect; use crate::keywords::{self, Keyword}; use crate::parser::{Parser, ParserError}; @@ -149,11 +151,11 @@ impl MsSqlDialect { start_token: AttachedToken(if_token), condition: Some(condition), then_token: None, - conditional_statements: ConditionalStatements::BeginEnd { + conditional_statements: ConditionalStatements::BeginEnd(BeginEndStatements { begin_token: AttachedToken(begin_token), statements, end_token: AttachedToken(end_token), - }, + }), } } else { let stmt = parser.parse_statement()?; @@ -167,8 +169,10 @@ impl MsSqlDialect { } }; + let mut prior_statement_ended_with_semi_colon = false; while let Token::SemiColon = parser.peek_token_ref().token { parser.advance_token(); + prior_statement_ended_with_semi_colon = true; } let mut else_block = None; @@ -182,11 +186,11 @@ impl MsSqlDialect { start_token: AttachedToken(else_token), condition: None, then_token: None, - conditional_statements: ConditionalStatements::BeginEnd { + conditional_statements: ConditionalStatements::BeginEnd(BeginEndStatements { begin_token: AttachedToken(begin_token), statements, end_token: AttachedToken(end_token), - }, + }), }); } else { let stmt = parser.parse_statement()?; @@ -199,6 +203,8 @@ impl MsSqlDialect { }, }); } + } else if prior_statement_ended_with_semi_colon { + parser.prev_token(); } Ok(Statement::If(IfStatement { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 77466b97..9b519fe8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -577,13 +577,7 @@ impl<'a> Parser<'a> { Keyword::GRANT => self.parse_grant(), Keyword::REVOKE => self.parse_revoke(), Keyword::START => self.parse_start_transaction(), - // `BEGIN` is a nonstandard but common alias for the - // standard `START TRANSACTION` statement. It is supported - // by at least PostgreSQL and MySQL. Keyword::BEGIN => self.parse_begin(), - // `END` is a nonstandard but common alias for the - // standard `COMMIT TRANSACTION` statement. It is supported - // by PostgreSQL. Keyword::END => self.parse_end(), Keyword::SAVEPOINT => self.parse_savepoint(), Keyword::RELEASE => self.parse_release(), @@ -618,6 +612,7 @@ impl<'a> Parser<'a> { // `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(), Keyword::PRINT => self.parse_print(), + Keyword::RETURN => self.parse_return(), _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -4458,7 +4453,6 @@ impl<'a> Parser<'a> { break; } } - values.push(self.parse_statement()?); self.expect_token(&Token::SemiColon)?; } @@ -4560,7 +4554,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::EXTERNAL) { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { - self.parse_create_function(or_replace, temporary) + self.parse_create_function(or_alter, or_replace, temporary) } else if self.parse_keyword(Keyword::TRIGGER) { self.parse_create_trigger(or_replace, false) } else if self.parse_keywords(&[Keyword::CONSTRAINT, Keyword::TRIGGER]) { @@ -4869,6 +4863,7 @@ impl<'a> Parser<'a> { pub fn parse_create_function( &mut self, + or_alter: bool, or_replace: bool, temporary: bool, ) -> Result { @@ -4880,6 +4875,8 @@ impl<'a> Parser<'a> { self.parse_create_macro(or_replace, temporary) } else if dialect_of!(self is BigQueryDialect) { self.parse_bigquery_create_function(or_replace, temporary) + } else if dialect_of!(self is MsSqlDialect) { + self.parse_mssql_create_function(or_alter, or_replace, temporary) } else { self.prev_token(); self.expected("an object type after CREATE", self.peek_token()) @@ -4994,6 +4991,7 @@ impl<'a> Parser<'a> { } Ok(Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace, temporary, name, @@ -5027,6 +5025,7 @@ impl<'a> Parser<'a> { let using = self.parse_optional_create_function_using()?; Ok(Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace, temporary, name, @@ -5054,22 +5053,7 @@ impl<'a> Parser<'a> { temporary: bool, ) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let name = self.parse_object_name(false)?; - - let parse_function_param = - |parser: &mut Parser| -> Result { - let name = parser.parse_identifier()?; - let data_type = parser.parse_data_type()?; - Ok(OperateFunctionArg { - mode: None, - name: Some(name), - data_type, - default_expr: None, - }) - }; - self.expect_token(&Token::LParen)?; - let args = self.parse_comma_separated0(parse_function_param, Token::RParen)?; - self.expect_token(&Token::RParen)?; + let (name, args) = self.parse_create_function_name_and_params()?; let return_type = if self.parse_keyword(Keyword::RETURNS) { Some(self.parse_data_type()?) @@ -5116,6 +5100,7 @@ impl<'a> Parser<'a> { }; Ok(Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace, temporary, if_not_exists, @@ -5134,6 +5119,73 @@ impl<'a> Parser<'a> { })) } + /// Parse `CREATE FUNCTION` for [MsSql] + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql + fn parse_mssql_create_function( + &mut self, + or_alter: bool, + or_replace: bool, + temporary: bool, + ) -> Result { + let (name, args) = self.parse_create_function_name_and_params()?; + + self.expect_keyword(Keyword::RETURNS)?; + let return_type = Some(self.parse_data_type()?); + + self.expect_keyword_is(Keyword::AS)?; + + let begin_token = self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(&[Keyword::END])?; + let end_token = self.expect_keyword(Keyword::END)?; + + let function_body = Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + })); + + Ok(Statement::CreateFunction(CreateFunction { + or_alter, + or_replace, + temporary, + if_not_exists: false, + name, + args: Some(args), + return_type, + function_body, + language: None, + determinism_specifier: None, + options: None, + remote_connection: None, + using: None, + behavior: None, + called_on_null: None, + parallel: None, + })) + } + + fn parse_create_function_name_and_params( + &mut self, + ) -> Result<(ObjectName, Vec), ParserError> { + let name = self.parse_object_name(false)?; + let parse_function_param = + |parser: &mut Parser| -> Result { + let name = parser.parse_identifier()?; + let data_type = parser.parse_data_type()?; + Ok(OperateFunctionArg { + mode: None, + name: Some(name), + data_type, + default_expr: None, + }) + }; + self.expect_token(&Token::LParen)?; + let args = self.parse_comma_separated0(parse_function_param, Token::RParen)?; + self.expect_token(&Token::RParen)?; + Ok((name, args)) + } + fn parse_function_arg(&mut self) -> Result { let mode = if self.parse_keyword(Keyword::IN) { Some(ArgMode::In) @@ -15161,6 +15213,16 @@ impl<'a> Parser<'a> { })) } + /// Parse [Statement::Return] + fn parse_return(&mut self) -> Result { + match self.maybe_parse(|p| p.parse_expr())? { + Some(expr) => Ok(Statement::Return(ReturnStatement { + value: Some(ReturnStatementValue::Expr(expr)), + })), + None => Ok(Statement::Return(ReturnStatement { value: None })), + } + } + /// Consume the parser and return its underlying token buffer pub fn into_tokens(self) -> Vec { self.tokens diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 5eb30d15..416d2e43 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2134,6 +2134,7 @@ fn test_bigquery_create_function() { assert_eq!( stmt, Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace: true, temporary: true, if_not_exists: false, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 466b65ec..66800b81 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15079,3 +15079,11 @@ fn parse_set_time_zone_alias() { _ => unreachable!(), } } + +#[test] +fn parse_return() { + let stmt = all_dialects().verified_stmt("RETURN"); + assert_eq!(stmt, Statement::Return(ReturnStatement { value: None })); + + let _ = all_dialects().verified_stmt("RETURN 1"); +} diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 2af93db7..9b043094 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -25,7 +25,7 @@ use sqlparser::ast::{ Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OrderByExpr, OrderByOptions, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, Value, }; -use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; +use sqlparser::dialect::{AnsiDialect, GenericDialect, HiveDialect}; use sqlparser::parser::ParserError; use sqlparser::test_utils::*; @@ -423,7 +423,7 @@ fn parse_create_function() { } // Test error in dialect that doesn't support parsing CREATE FUNCTION - let unsupported_dialects = TestedDialects::new(vec![Box::new(MsSqlDialect {})]); + let unsupported_dialects = TestedDialects::new(vec![Box::new(AnsiDialect {})]); assert_eq!( unsupported_dialects.parse_sql_statements(sql).unwrap_err(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2786384b..b86e1a7d 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -187,6 +187,92 @@ fn parse_mssql_create_procedure() { let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; SELECT [foo] FROM BAR WHERE [FOO] > 10 END"); } +#[test] +fn parse_create_function() { + let return_expression_function = "CREATE FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) RETURNS INT AS BEGIN RETURN 1; END"; + assert_eq!( + ms().verified_stmt(return_expression_function), + sqlparser::ast::Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: false, + temporary: false, + if_not_exists: false, + name: ObjectName::from(vec![Ident::new("some_scalar_udf")]), + args: Some(vec![ + OperateFunctionArg { + mode: None, + name: Some(Ident::new("@foo")), + data_type: DataType::Int(None), + default_expr: None, + }, + OperateFunctionArg { + mode: None, + name: Some(Ident::new("@bar")), + data_type: DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 256, + unit: None + })), + default_expr: None, + }, + ]), + return_type: Some(DataType::Int(None)), + function_body: Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { + begin_token: AttachedToken::empty(), + statements: vec![Statement::Return(ReturnStatement { + value: Some(ReturnStatementValue::Expr(Expr::Value( + (number("1")).with_empty_span() + ))), + }),], + end_token: AttachedToken::empty(), + })), + behavior: None, + called_on_null: None, + parallel: None, + using: None, + language: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }), + ); + + let multi_statement_function = "\ + CREATE FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) \ + RETURNS INT \ + AS \ + BEGIN \ + SET @foo = @foo + 1; \ + RETURN @foo; \ + END\ + "; + let _ = ms().verified_stmt(multi_statement_function); + + let create_function_with_conditional = "\ + CREATE FUNCTION some_scalar_udf() \ + RETURNS INT \ + AS \ + BEGIN \ + IF 1 = 2 \ + BEGIN \ + RETURN 1; \ + END; \ + RETURN 0; \ + END\ + "; + let _ = ms().verified_stmt(create_function_with_conditional); + + let create_or_alter_function = "\ + CREATE OR ALTER FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) \ + RETURNS INT \ + AS \ + BEGIN \ + SET @foo = @foo + 1; \ + RETURN @foo; \ + END\ + "; + let _ = ms().verified_stmt(create_or_alter_function); +} + #[test] fn parse_mssql_apply_join() { let _ = ms_and_generic().verified_only_select( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 098d4b1c..27fc7fa1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4104,6 +4104,7 @@ fn parse_create_function() { assert_eq!( pg_and_generic().verified_stmt(sql), Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace: false, temporary: false, name: ObjectName::from(vec![Ident::new("add")]), @@ -5485,6 +5486,7 @@ fn parse_trigger_related_functions() { assert_eq!( create_function, Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace: false, temporary: false, if_not_exists: false, From 87d190734c7b978e8252b110c9529d7a93a30cf0 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Wed, 23 Apr 2025 12:55:57 -0400 Subject: [PATCH 045/130] Add `OR ALTER` support for `CREATE VIEW` (#1818) --- src/ast/mod.rs | 8 +++++++- src/ast/spans.rs | 1 + src/parser/mod.rs | 4 +++- tests/sqlparser_common.rs | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c4bb3fa1..b60ade78 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3063,6 +3063,10 @@ pub enum Statement { /// CREATE VIEW /// ``` CreateView { + /// True if this is a `CREATE OR ALTER VIEW` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql) + or_alter: bool, or_replace: bool, materialized: bool, /// View name @@ -4623,6 +4627,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateView { + or_alter, name, or_replace, columns, @@ -4639,7 +4644,8 @@ impl fmt::Display for Statement { } => { write!( f, - "CREATE {or_replace}", + "CREATE {or_alter}{or_replace}", + or_alter = if *or_alter { "OR ALTER " } else { "" }, or_replace = if *or_replace { "OR REPLACE " } else { "" }, )?; if let Some(params) = params { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 16ff660d..93de5fff 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -384,6 +384,7 @@ impl Spanned for Statement { ), Statement::Delete(delete) => delete.span(), Statement::CreateView { + or_alter: _, or_replace: _, materialized: _, name, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9b519fe8..fe81b599 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4548,7 +4548,7 @@ impl<'a> Parser<'a> { self.parse_create_table(or_replace, temporary, global, transient) } else if self.parse_keyword(Keyword::MATERIALIZED) || self.parse_keyword(Keyword::VIEW) { self.prev_token(); - self.parse_create_view(or_replace, temporary, create_view_params) + self.parse_create_view(or_alter, or_replace, temporary, create_view_params) } else if self.parse_keyword(Keyword::POLICY) { self.parse_create_policy() } else if self.parse_keyword(Keyword::EXTERNAL) { @@ -5512,6 +5512,7 @@ impl<'a> Parser<'a> { pub fn parse_create_view( &mut self, + or_alter: bool, or_replace: bool, temporary: bool, create_view_params: Option, @@ -5576,6 +5577,7 @@ impl<'a> Parser<'a> { ]); Ok(Statement::CreateView { + or_alter, name, columns, query, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 66800b81..fa2346c2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7840,6 +7840,7 @@ fn parse_create_view() { let sql = "CREATE VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { Statement::CreateView { + or_alter, name, columns, query, @@ -7854,6 +7855,7 @@ fn parse_create_view() { to, params, } => { + assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -7870,6 +7872,8 @@ fn parse_create_view() { } _ => unreachable!(), } + + let _ = verified_stmt("CREATE OR ALTER VIEW v AS SELECT 1"); } #[test] @@ -7904,6 +7908,7 @@ fn parse_create_view_with_columns() { // match all_dialects().verified_stmt(sql) { match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::CreateView { + or_alter, name, columns, or_replace, @@ -7918,6 +7923,7 @@ fn parse_create_view_with_columns() { to, params, } => { + assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!( columns, @@ -7951,6 +7957,7 @@ fn parse_create_view_temporary() { let sql = "CREATE TEMPORARY VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { Statement::CreateView { + or_alter, name, columns, query, @@ -7965,6 +7972,7 @@ fn parse_create_view_temporary() { to, params, } => { + assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -7988,6 +7996,7 @@ fn parse_create_or_replace_view() { let sql = "CREATE OR REPLACE VIEW v AS SELECT 1"; match verified_stmt(sql) { Statement::CreateView { + or_alter, name, columns, or_replace, @@ -8002,6 +8011,7 @@ fn parse_create_or_replace_view() { to, params, } => { + assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(options, CreateTableOptions::None); @@ -8029,6 +8039,7 @@ fn parse_create_or_replace_materialized_view() { let sql = "CREATE OR REPLACE MATERIALIZED VIEW v AS SELECT 1"; match verified_stmt(sql) { Statement::CreateView { + or_alter, name, columns, or_replace, @@ -8043,6 +8054,7 @@ fn parse_create_or_replace_materialized_view() { to, params, } => { + assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(options, CreateTableOptions::None); @@ -8066,6 +8078,7 @@ fn parse_create_materialized_view() { let sql = "CREATE MATERIALIZED VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { Statement::CreateView { + or_alter, name, or_replace, columns, @@ -8080,6 +8093,7 @@ fn parse_create_materialized_view() { to, params, } => { + assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -8103,6 +8117,7 @@ fn parse_create_materialized_view_with_cluster_by() { let sql = "CREATE MATERIALIZED VIEW myschema.myview CLUSTER BY (foo) AS SELECT foo FROM bar"; match verified_stmt(sql) { Statement::CreateView { + or_alter, name, or_replace, columns, @@ -8117,6 +8132,7 @@ fn parse_create_materialized_view_with_cluster_by() { to, params, } => { + assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); From 7703fd0d3180c2e8b347c11394084c3a2458be14 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 24 Apr 2025 14:16:49 -0400 Subject: [PATCH 046/130] Add `DECLARE ... CURSOR FOR` support for SQL Server (#1821) --- src/ast/mod.rs | 3 ++- src/parser/mod.rs | 22 +++++++++++++++++----- tests/sqlparser_mssql.rs | 4 ++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b60ade78..45924579 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2472,10 +2472,11 @@ impl fmt::Display for DeclareAssignment { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum DeclareType { - /// Cursor variable type. e.g. [Snowflake] [PostgreSQL] + /// Cursor variable type. e.g. [Snowflake] [PostgreSQL] [MsSql] /// /// [Snowflake]: https://docs.snowflake.com/en/developer-guide/snowflake-scripting/cursors#declaring-a-cursor /// [PostgreSQL]: https://www.postgresql.org/docs/current/plpgsql-cursors.html + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-cursor-transact-sql Cursor, /// Result set variable type. [Snowflake] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fe81b599..0546548a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6446,7 +6446,7 @@ impl<'a> Parser<'a> { /// DECLARE // { // { @local_variable [AS] data_type [ = value ] } - // | { @cursor_variable_name CURSOR } + // | { @cursor_variable_name CURSOR [ FOR ] } // } [ ,...n ] /// ``` /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-local-variable-transact-sql?view=sql-server-ver16 @@ -6462,14 +6462,19 @@ impl<'a> Parser<'a> { /// ```text // { // { @local_variable [AS] data_type [ = value ] } - // | { @cursor_variable_name CURSOR } + // | { @cursor_variable_name CURSOR [ FOR ]} // } [ ,...n ] /// ``` /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-local-variable-transact-sql?view=sql-server-ver16 pub fn parse_mssql_declare_stmt(&mut self) -> Result { let name = { let ident = self.parse_identifier()?; - if !ident.value.starts_with('@') { + if !ident.value.starts_with('@') + && !matches!( + self.peek_token().token, + Token::Word(w) if w.keyword == Keyword::CURSOR + ) + { Err(ParserError::TokenizerError( "Invalid MsSql variable declaration.".to_string(), )) @@ -6493,7 +6498,14 @@ impl<'a> Parser<'a> { _ => (None, Some(self.parse_data_type()?)), }; - let assignment = self.parse_mssql_variable_declaration_expression()?; + let (for_query, assignment) = if self.peek_keyword(Keyword::FOR) { + self.next_token(); + let query = Some(self.parse_query()?); + (query, None) + } else { + let assignment = self.parse_mssql_variable_declaration_expression()?; + (None, assignment) + }; Ok(Declare { names: vec![name], @@ -6504,7 +6516,7 @@ impl<'a> Parser<'a> { sensitive: None, scroll: None, hold: None, - for_query: None, + for_query, }) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b86e1a7d..ef610347 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1387,6 +1387,10 @@ fn parse_mssql_declare() { ], ast ); + + let declare_cursor_for_select = + "DECLARE vend_cursor CURSOR FOR SELECT * FROM Purchasing.Vendor"; + let _ = ms().verified_stmt(declare_cursor_for_select); } #[test] From 4e392f5c07ef1855c5de3ddb604be5d718ab1040 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Mon, 28 Apr 2025 19:03:39 +0200 Subject: [PATCH 047/130] Handle missing login in changelog generate script (#1823) --- dev/release/generate-changelog.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev/release/generate-changelog.py b/dev/release/generate-changelog.py index 52fd2e54..6f2b7c41 100755 --- a/dev/release/generate-changelog.py +++ b/dev/release/generate-changelog.py @@ -28,7 +28,8 @@ def print_pulls(repo_name, title, pulls): print() for (pull, commit) in pulls: url = "https://github.com/{}/pull/{}".format(repo_name, pull.number) - print("- {} [#{}]({}) ({})".format(pull.title, pull.number, url, commit.author.login)) + author = f"({commit.author.login})" if commit.author else '' + print("- {} [#{}]({}) {}".format(pull.title, pull.number, url, author)) print() @@ -161,4 +162,4 @@ def cli(args=None): generate_changelog(repo, project, args.tag1, args.tag2, args.version) if __name__ == "__main__": - cli() \ No newline at end of file + cli() From 2b5bdcef0b79274f8987d4c5d71cbc861dcc50cd Mon Sep 17 00:00:00 2001 From: tomershaniii <65544633+tomershaniii@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:44:19 +0300 Subject: [PATCH 048/130] Snowflake: Add support for `CONNECT_BY_ROOT` (#1780) --- src/ast/mod.rs | 10 ++++--- src/ast/spans.rs | 2 +- src/dialect/mod.rs | 6 +++++ src/dialect/snowflake.rs | 6 +++++ src/keywords.rs | 1 + src/parser/mod.rs | 51 +++++++++++++++++++++++++++--------- tests/sqlparser_mysql.rs | 9 ++++--- tests/sqlparser_snowflake.rs | 42 +++++++++++++++++++++++++++++ 8 files changed, 107 insertions(+), 20 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 45924579..d6588981 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -930,12 +930,14 @@ pub enum Expr { Nested(Box), /// A literal value, such as string, number, date or NULL Value(ValueWithSpan), + /// Prefixed expression, e.g. introducer strings, projection prefix /// - IntroducedString { - introducer: String, + /// + Prefixed { + prefix: Ident, /// The value of the constant. /// Hint: you can unwrap the string value using `value.into_string()`. - value: Value, + value: Box, }, /// A constant of form ` 'value'`. /// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`), @@ -1655,7 +1657,7 @@ impl fmt::Display for Expr { Expr::Collate { expr, collation } => write!(f, "{expr} COLLATE {collation}"), Expr::Nested(ast) => write!(f, "({ast})"), Expr::Value(v) => write!(f, "{v}"), - Expr::IntroducedString { introducer, value } => write!(f, "{introducer} {value}"), + Expr::Prefixed { prefix, value } => write!(f, "{prefix} {value}"), Expr::TypedString { data_type, value } => { write!(f, "{data_type}")?; write!(f, " {value}") diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 93de5fff..28d479f3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1543,7 +1543,7 @@ impl Spanned for Expr { .map(|items| union_spans(items.iter().map(|i| i.span()))), ), ), - Expr::IntroducedString { value, .. } => value.span(), + Expr::Prefixed { value, .. } => value.span(), Expr::Case { operand, conditions, diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index e41964f4..b2dff065 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -888,6 +888,12 @@ pub trait Dialect: Debug + Any { keywords::RESERVED_FOR_TABLE_FACTOR } + /// Returns reserved keywords that may prefix a select item expression + /// e.g. `SELECT CONNECT_BY_ROOT name FROM Tbl2` (Snowflake) + fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { + &[] + } + /// Returns true if this dialect supports the `TABLESAMPLE` option /// before the table alias option. For example: /// diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 8b279c7c..c4d6a5ad 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -44,6 +44,7 @@ use alloc::{format, vec}; use super::keywords::RESERVED_FOR_IDENTIFIER; use sqlparser::ast::StorageSerializationPolicy; +const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT]; /// A [`Dialect`] for [Snowflake](https://www.snowflake.com/) #[derive(Debug, Default)] pub struct SnowflakeDialect; @@ -346,6 +347,11 @@ impl Dialect for SnowflakeDialect { fn supports_group_by_expr(&self) -> bool { true } + + /// See: + fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { + &RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/keywords.rs b/src/keywords.rs index 4eaad7ed..15a6f91a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -207,6 +207,7 @@ define_keywords!( CONNECT, CONNECTION, CONNECTOR, + CONNECT_BY_ROOT, CONSTRAINT, CONTAINS, CONTINUE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0546548a..03ea91fa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1388,9 +1388,9 @@ impl<'a> Parser<'a> { | Token::HexStringLiteral(_) if w.value.starts_with('_') => { - Ok(Expr::IntroducedString { - introducer: w.value.clone(), - value: self.parse_introduced_string_value()?, + Ok(Expr::Prefixed { + prefix: w.clone().into_ident(w_span), + value: self.parse_introduced_string_expr()?.into(), }) } // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html @@ -1399,9 +1399,9 @@ impl<'a> Parser<'a> { | Token::HexStringLiteral(_) if w.value.starts_with('_') => { - Ok(Expr::IntroducedString { - introducer: w.value.clone(), - value: self.parse_introduced_string_value()?, + Ok(Expr::Prefixed { + prefix: w.clone().into_ident(w_span), + value: self.parse_introduced_string_expr()?.into(), }) } Token::Arrow if self.dialect.supports_lambda_functions() => { @@ -9035,13 +9035,19 @@ impl<'a> Parser<'a> { } } - fn parse_introduced_string_value(&mut self) -> Result { + fn parse_introduced_string_expr(&mut self) -> Result { let next_token = self.next_token(); let span = next_token.span; match next_token.token { - Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), - Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), - Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), + Token::SingleQuotedString(ref s) => Ok(Expr::Value( + Value::SingleQuotedString(s.to_string()).with_span(span), + )), + Token::DoubleQuotedString(ref s) => Ok(Expr::Value( + Value::DoubleQuotedString(s.to_string()).with_span(span), + )), + Token::HexStringLiteral(ref s) => Ok(Expr::Value( + Value::HexStringLiteral(s.to_string()).with_span(span), + )), unexpected => self.expected( "a string value", TokenWithSpan { @@ -13968,6 +13974,13 @@ impl<'a> Parser<'a> { /// Parse a comma-delimited list of projections after SELECT pub fn parse_select_item(&mut self) -> Result { + let prefix = self + .parse_one_of_keywords( + self.dialect + .get_reserved_keywords_for_select_item_operator(), + ) + .map(|keyword| Ident::new(format!("{:?}", keyword))); + match self.parse_wildcard_expr()? { Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard( SelectItemQualifiedWildcardKind::ObjectName(prefix), @@ -14012,8 +14025,11 @@ impl<'a> Parser<'a> { expr => self .maybe_parse_select_item_alias() .map(|alias| match alias { - Some(alias) => SelectItem::ExprWithAlias { expr, alias }, - None => SelectItem::UnnamedExpr(expr), + Some(alias) => SelectItem::ExprWithAlias { + expr: maybe_prefixed_expr(expr, prefix), + alias, + }, + None => SelectItem::UnnamedExpr(maybe_prefixed_expr(expr, prefix)), }), } } @@ -15375,6 +15391,17 @@ impl<'a> Parser<'a> { } } +fn maybe_prefixed_expr(expr: Expr, prefix: Option) -> Expr { + if let Some(prefix) = prefix { + Expr::Prefixed { + prefix, + value: Box::new(expr), + } + } else { + expr + } +} + impl Word { #[deprecated(since = "0.54.0", note = "please use `into_ident` instead")] pub fn to_ident(&self, span: Span) -> Ident { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 9d8d12b5..f74248b8 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3020,9 +3020,12 @@ fn parse_hex_string_introducer() { distinct: None, top: None, top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr(Expr::IntroducedString { - introducer: "_latin1".to_string(), - value: Value::HexStringLiteral("4D7953514C".to_string()) + projection: vec![SelectItem::UnnamedExpr(Expr::Prefixed { + prefix: Ident::from("_latin1"), + value: Expr::Value( + Value::HexStringLiteral("4D7953514C".to_string()).with_empty_span() + ) + .into(), })], from: vec![], lateral_views: vec![], diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 84c08874..aa974115 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3983,3 +3983,45 @@ fn test_nested_join_without_parentheses() { }], ); } + +#[test] +fn parse_connect_by_root_operator() { + let sql = "SELECT CONNECT_BY_ROOT name AS root_name FROM Tbl1"; + + match snowflake().verified_stmt(sql) { + Statement::Query(query) => { + assert_eq!( + query.body.as_select().unwrap().projection[0], + SelectItem::ExprWithAlias { + expr: Expr::Prefixed { + prefix: Ident::new("CONNECT_BY_ROOT"), + value: Box::new(Expr::Identifier(Ident::new("name"))) + }, + alias: Ident::new("root_name"), + } + ); + } + _ => unreachable!(), + } + + let sql = "SELECT CONNECT_BY_ROOT name FROM Tbl2"; + match snowflake().verified_stmt(sql) { + Statement::Query(query) => { + assert_eq!( + query.body.as_select().unwrap().projection[0], + SelectItem::UnnamedExpr(Expr::Prefixed { + prefix: Ident::new("CONNECT_BY_ROOT"), + value: Box::new(Expr::Identifier(Ident::new("name"))) + }) + ); + } + _ => unreachable!(), + } + + let sql = "SELECT CONNECT_BY_ROOT FROM Tbl2"; + let res = snowflake().parse_sql_statements(sql); + assert_eq!( + res.unwrap_err().to_string(), + "sql parser error: Expected an expression, found: FROM" + ); +} From c0921dceb93218ca97bf7e0d65f1f28d7729289d Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Tue, 29 Apr 2025 21:32:04 +0200 Subject: [PATCH 049/130] Prepare for 0.56.0 release: Version and CHANGELOG (#1822) --- CHANGELOG.md | 1 + Cargo.toml | 2 +- changelog/0.56.0.md | 100 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 changelog/0.56.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 362a637d..a5511a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ technically be breaking and thus will result in a `0.(N+1)` version. - Unreleased: Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +- `0.56.0`: [changelog/0.56.0.md](changelog/0.56.0.md) - `0.55.0`: [changelog/0.55.0.md](changelog/0.55.0.md) - `0.54.0`: [changelog/0.54.0.md](changelog/0.54.0.md) - `0.53.0`: [changelog/0.53.0.md](changelog/0.53.0.md) diff --git a/Cargo.toml b/Cargo.toml index 99bfdc24..d746775e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.55.0" +version = "0.56.0" authors = ["Apache DataFusion "] homepage = "https://github.com/apache/datafusion-sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/changelog/0.56.0.md b/changelog/0.56.0.md new file mode 100644 index 00000000..9e6289e8 --- /dev/null +++ b/changelog/0.56.0.md @@ -0,0 +1,100 @@ + + +# sqlparser-rs 0.56.0 Changelog + +This release consists of 45 commits from 19 contributors. See credits at the end of this changelog for more information. + +**Other:** + +- Ignore escaped LIKE wildcards in MySQL [#1735](https://github.com/apache/datafusion-sqlparser-rs/pull/1735) (mvzink) +- Parse SET NAMES syntax in Postgres [#1752](https://github.com/apache/datafusion-sqlparser-rs/pull/1752) (mvzink) +- re-add support for nested comments in mssql [#1754](https://github.com/apache/datafusion-sqlparser-rs/pull/1754) (lovasoa) +- Extend support for INDEX parsing [#1707](https://github.com/apache/datafusion-sqlparser-rs/pull/1707) (LucaCappelletti94) +- Parse MySQL `ALTER TABLE DROP FOREIGN KEY` syntax [#1762](https://github.com/apache/datafusion-sqlparser-rs/pull/1762) (mvzink) +- add support for `with` clauses (CTEs) in `delete` statements [#1764](https://github.com/apache/datafusion-sqlparser-rs/pull/1764) (lovasoa) +- SET with a list of comma separated assignments [#1757](https://github.com/apache/datafusion-sqlparser-rs/pull/1757) (MohamedAbdeen21) +- Preserve MySQL-style `LIMIT , ` syntax [#1765](https://github.com/apache/datafusion-sqlparser-rs/pull/1765) (mvzink) +- Add support for `DROP MATERIALIZED VIEW` [#1743](https://github.com/apache/datafusion-sqlparser-rs/pull/1743) (iffyio) +- Add `CASE` and `IF` statement support [#1741](https://github.com/apache/datafusion-sqlparser-rs/pull/1741) (iffyio) +- BigQuery: Add support for `CREATE SCHEMA` options [#1742](https://github.com/apache/datafusion-sqlparser-rs/pull/1742) (iffyio) +- Snowflake: Support dollar quoted comments [#1755](https://github.com/apache/datafusion-sqlparser-rs/pull/1755) +- Add LOCK operation for ALTER TABLE [#1768](https://github.com/apache/datafusion-sqlparser-rs/pull/1768) (MohamedAbdeen21) +- Add support for `RAISE` statement [#1766](https://github.com/apache/datafusion-sqlparser-rs/pull/1766) (iffyio) +- Add GLOBAL context/modifier to SET statements [#1767](https://github.com/apache/datafusion-sqlparser-rs/pull/1767) (MohamedAbdeen21) +- Parse `SUBSTR` as alias for `SUBSTRING` [#1769](https://github.com/apache/datafusion-sqlparser-rs/pull/1769) (mvzink) +- SET statements: scope modifier for multiple assignments [#1772](https://github.com/apache/datafusion-sqlparser-rs/pull/1772) (MohamedAbdeen21) +- Support qualified column names in `MATCH AGAINST` clause [#1774](https://github.com/apache/datafusion-sqlparser-rs/pull/1774) (tomershaniii) +- Mysql: Add support for := operator [#1779](https://github.com/apache/datafusion-sqlparser-rs/pull/1779) (barsela1) +- Add cipherstash-proxy to list of users in README.md [#1782](https://github.com/apache/datafusion-sqlparser-rs/pull/1782) (coderdan) +- Fix typos [#1785](https://github.com/apache/datafusion-sqlparser-rs/pull/1785) (jayvdb) +- Add support for Databricks TIMESTAMP_NTZ. [#1781](https://github.com/apache/datafusion-sqlparser-rs/pull/1781) (romanb) +- Enable double-dot-notation for mssql. [#1787](https://github.com/apache/datafusion-sqlparser-rs/pull/1787) (romanb) +- Fix: Snowflake ALTER SESSION cannot be followed by other statements. [#1786](https://github.com/apache/datafusion-sqlparser-rs/pull/1786) (romanb) +- Add GreptimeDB to the "Users" in README [#1788](https://github.com/apache/datafusion-sqlparser-rs/pull/1788) (MichaelScofield) +- Extend snowflake grant options support [#1794](https://github.com/apache/datafusion-sqlparser-rs/pull/1794) (yoavcloud) +- Fix clippy lint on rust 1.86 [#1796](https://github.com/apache/datafusion-sqlparser-rs/pull/1796) (iffyio) +- Allow single quotes in EXTRACT() for Redshift. [#1795](https://github.com/apache/datafusion-sqlparser-rs/pull/1795) (romanb) +- MSSQL: Add support for functionality `MERGE` output clause [#1790](https://github.com/apache/datafusion-sqlparser-rs/pull/1790) (dilovancelik) +- Support additional DuckDB integer types such as HUGEINT, UHUGEINT, etc [#1797](https://github.com/apache/datafusion-sqlparser-rs/pull/1797) (alexander-beedie) +- Add support for MSSQL IF/ELSE statements. [#1791](https://github.com/apache/datafusion-sqlparser-rs/pull/1791) (romanb) +- Allow literal backslash escapes for string literals in Redshift dialect. [#1801](https://github.com/apache/datafusion-sqlparser-rs/pull/1801) (romanb) +- Add support for MySQL's STRAIGHT_JOIN join operator. [#1802](https://github.com/apache/datafusion-sqlparser-rs/pull/1802) (romanb) +- Snowflake COPY INTO target columns, select items and optional alias [#1805](https://github.com/apache/datafusion-sqlparser-rs/pull/1805) (yoavcloud) +- Fix tokenization of qualified identifiers with numeric prefix. [#1803](https://github.com/apache/datafusion-sqlparser-rs/pull/1803) (romanb) +- Add support for `INHERITS` option in `CREATE TABLE` statement [#1806](https://github.com/apache/datafusion-sqlparser-rs/pull/1806) (LucaCappelletti94) +- Add `DROP TRIGGER` support for SQL Server [#1813](https://github.com/apache/datafusion-sqlparser-rs/pull/1813) (aharpervc) +- Snowflake: support nested join without parentheses [#1799](https://github.com/apache/datafusion-sqlparser-rs/pull/1799) (barsela1) +- Add support for parenthesized subquery as `IN` predicate [#1793](https://github.com/apache/datafusion-sqlparser-rs/pull/1793) (adamchainz) +- Fix `STRAIGHT_JOIN` constraint when table alias is absent [#1812](https://github.com/apache/datafusion-sqlparser-rs/pull/1812) (killertux) +- Add support for `PRINT` statement for SQL Server [#1811](https://github.com/apache/datafusion-sqlparser-rs/pull/1811) (aharpervc) +- enable `supports_filter_during_aggregation` for Generic dialect [#1815](https://github.com/apache/datafusion-sqlparser-rs/pull/1815) (goldmedal) +- Add support for `XMLTABLE` [#1817](https://github.com/apache/datafusion-sqlparser-rs/pull/1817) (lovasoa) +- Add `CREATE FUNCTION` support for SQL Server [#1808](https://github.com/apache/datafusion-sqlparser-rs/pull/1808) (aharpervc) +- Add `OR ALTER` support for `CREATE VIEW` [#1818](https://github.com/apache/datafusion-sqlparser-rs/pull/1818) (aharpervc) +- Add `DECLARE ... CURSOR FOR` support for SQL Server [#1821](https://github.com/apache/datafusion-sqlparser-rs/pull/1821) (aharpervc) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 8 Roman Borschel + 5 Ifeanyi Ubah + 5 Michael Victor Zink + 4 Andrew Harper + 4 Mohamed Abdeen + 3 Ophir LOJKINE + 2 Luca Cappelletti + 2 Yoav Cohen + 2 bar sela + 1 Adam Johnson + 1 Aleksei Piianin + 1 Alexander Beedie + 1 Bruno Clemente + 1 Dan Draper + 1 DilovanCelik + 1 Jax Liu + 1 John Vandenberg + 1 LFC + 1 tomershaniii +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. + From a5b9821d1d2fa9c0b8ee73b698a2b0e5f138beaf Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 29 Apr 2025 15:55:22 -0400 Subject: [PATCH 050/130] Update `56.0.0` Changelog with latest commits (#1832) --- changelog/0.56.0.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/changelog/0.56.0.md b/changelog/0.56.0.md index 9e6289e8..b3c8a67a 100644 --- a/changelog/0.56.0.md +++ b/changelog/0.56.0.md @@ -19,7 +19,7 @@ under the License. # sqlparser-rs 0.56.0 Changelog -This release consists of 45 commits from 19 contributors. See credits at the end of this changelog for more information. +This release consists of 48 commits from 19 contributors. See credits at the end of this changelog for more information. **Other:** @@ -69,6 +69,8 @@ This release consists of 45 commits from 19 contributors. See credits at the end - Add `CREATE FUNCTION` support for SQL Server [#1808](https://github.com/apache/datafusion-sqlparser-rs/pull/1808) (aharpervc) - Add `OR ALTER` support for `CREATE VIEW` [#1818](https://github.com/apache/datafusion-sqlparser-rs/pull/1818) (aharpervc) - Add `DECLARE ... CURSOR FOR` support for SQL Server [#1821](https://github.com/apache/datafusion-sqlparser-rs/pull/1821) (aharpervc) +- Handle missing login in changelog generate script [#1823](https://github.com/apache/datafusion-sqlparser-rs/pull/1823) (iffyio) +- Snowflake: Add support for `CONNECT_BY_ROOT` [#1780](https://github.com/apache/datafusion-sqlparser-rs/pull/1780) (tomershaniii) ## Credits @@ -76,14 +78,15 @@ Thank you to everyone who contributed to this release. Here is a breakdown of co ``` 8 Roman Borschel - 5 Ifeanyi Ubah + 6 Ifeanyi Ubah + 5 Andrew Harper 5 Michael Victor Zink - 4 Andrew Harper 4 Mohamed Abdeen 3 Ophir LOJKINE 2 Luca Cappelletti 2 Yoav Cohen 2 bar sela + 2 tomershaniii 1 Adam Johnson 1 Aleksei Piianin 1 Alexander Beedie @@ -93,7 +96,6 @@ Thank you to everyone who contributed to this release. Here is a breakdown of co 1 Jax Liu 1 John Vandenberg 1 LFC - 1 tomershaniii ``` Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. From e5d2215267c000fbdb453a0345e9878311086269 Mon Sep 17 00:00:00 2001 From: Simon Vandel Sillesen Date: Fri, 2 May 2025 05:13:47 +0200 Subject: [PATCH 051/130] Support some of pipe operators (#1759) --- src/ast/mod.rs | 34 ++++---- src/ast/query.rs | 155 ++++++++++++++++++++++++++++++++++++ src/ast/spans.rs | 9 ++- src/dialect/bigquery.rs | 4 + src/dialect/mod.rs | 14 ++++ src/keywords.rs | 2 + src/parser/mod.rs | 116 +++++++++++++++++++++++++++ src/tokenizer.rs | 6 ++ tests/sqlparser_common.rs | 84 +++++++++++++++++++ tests/sqlparser_mssql.rs | 4 + tests/sqlparser_mysql.rs | 15 ++++ tests/sqlparser_postgres.rs | 5 ++ 12 files changed, 427 insertions(+), 21 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d6588981..b496403c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -66,23 +66,23 @@ pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, - ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml, - FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, - InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, - JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, - LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, - Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, - OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, PivotValueSource, - ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, - ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, - SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Setting, - SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, - TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample, - TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, - TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, - TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, - WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, XmlPassingArgument, - XmlPassingClause, XmlTableColumn, XmlTableColumnOption, + ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause, + ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, + IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, + JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, + JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, + MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, + OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, + PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, + SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator, + SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, + TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, TableIndexHints, + TableIndexType, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod, + TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, + TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, UpdateTableFromKind, + ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, + XmlPassingArgument, XmlPassingClause, XmlTableColumn, XmlTableColumnOption, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index 982985ec..a90b6166 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -62,6 +62,9 @@ pub struct Query { /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/format) /// (ClickHouse-specific) pub format_clause: Option, + + /// Pipe operator + pub pipe_operators: Vec, } impl fmt::Display for Query { @@ -92,6 +95,9 @@ impl fmt::Display for Query { if let Some(ref format) = self.format_clause { write!(f, " {}", format)?; } + for pipe_operator in &self.pipe_operators { + write!(f, " |> {}", pipe_operator)?; + } Ok(()) } } @@ -1004,6 +1010,26 @@ impl fmt::Display for ExprWithAlias { } } +/// An expression optionally followed by an alias and order by options. +/// +/// Example: +/// ```sql +/// 42 AS myint ASC +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExprWithAliasAndOrderBy { + pub expr: ExprWithAlias, + pub order_by: OrderByOptions, +} + +impl fmt::Display for ExprWithAliasAndOrderBy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", self.expr, self.order_by) + } +} + /// Arguments to a table-valued function #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -2513,6 +2539,135 @@ impl fmt::Display for OffsetRows { } } +/// Pipe syntax, first introduced in Google BigQuery. +/// Example: +/// +/// ```sql +/// FROM Produce +/// |> WHERE sales > 0 +/// |> AGGREGATE SUM(sales) AS total_sales, COUNT(*) AS num_sales +/// GROUP BY item; +/// ``` +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum PipeOperator { + /// Limits the number of rows to return in a query, with an optional OFFSET clause to skip over rows. + /// + /// Syntax: `|> LIMIT [OFFSET ]` + /// + /// See more at + Limit { expr: Expr, offset: Option }, + /// Filters the results of the input table. + /// + /// Syntax: `|> WHERE ` + /// + /// See more at + Where { expr: Expr }, + /// `ORDER BY [ASC|DESC], ...` + OrderBy { exprs: Vec }, + /// Produces a new table with the listed columns, similar to the outermost SELECT clause in a table subquery in standard syntax. + /// + /// Syntax `|> SELECT [[AS] alias], ...` + /// + /// See more at + Select { exprs: Vec }, + /// Propagates the existing table and adds computed columns, similar to SELECT *, new_column in standard syntax. + /// + /// Syntax: `|> EXTEND [[AS] alias], ...` + /// + /// See more at + Extend { exprs: Vec }, + /// Replaces the value of a column in the current table, similar to SELECT * REPLACE (expression AS column) in standard syntax. + /// + /// Syntax: `|> SET = , ...` + /// + /// See more at + Set { assignments: Vec }, + /// Removes listed columns from the current table, similar to SELECT * EXCEPT (column) in standard syntax. + /// + /// Syntax: `|> DROP , ...` + /// + /// See more at + Drop { columns: Vec }, + /// Introduces a table alias for the input table, similar to applying the AS alias clause on a table subquery in standard syntax. + /// + /// Syntax: `|> AS ` + /// + /// See more at + As { alias: Ident }, + /// Performs aggregation on data across grouped rows or an entire table. + /// + /// Syntax: `|> AGGREGATE [[AS] alias], ...` + /// + /// Syntax: + /// ```norust + /// |> AGGREGATE [ [[AS] alias], ...] + /// GROUP BY [AS alias], ... + /// ``` + /// + /// See more at + Aggregate { + full_table_exprs: Vec, + group_by_expr: Vec, + }, +} + +impl fmt::Display for PipeOperator { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PipeOperator::Select { exprs } => { + write!(f, "SELECT {}", display_comma_separated(exprs.as_slice())) + } + PipeOperator::Extend { exprs } => { + write!(f, "EXTEND {}", display_comma_separated(exprs.as_slice())) + } + PipeOperator::Set { assignments } => { + write!(f, "SET {}", display_comma_separated(assignments.as_slice())) + } + PipeOperator::Drop { columns } => { + write!(f, "DROP {}", display_comma_separated(columns.as_slice())) + } + PipeOperator::As { alias } => { + write!(f, "AS {}", alias) + } + PipeOperator::Limit { expr, offset } => { + write!(f, "LIMIT {}", expr)?; + if let Some(offset) = offset { + write!(f, " OFFSET {}", offset)?; + } + Ok(()) + } + PipeOperator::Aggregate { + full_table_exprs, + group_by_expr, + } => { + write!(f, "AGGREGATE")?; + if !full_table_exprs.is_empty() { + write!( + f, + " {}", + display_comma_separated(full_table_exprs.as_slice()) + )?; + } + if !group_by_expr.is_empty() { + write!(f, " GROUP BY {}", display_comma_separated(group_by_expr))?; + } + Ok(()) + } + + PipeOperator::Where { expr } => { + write!(f, "WHERE {}", expr) + } + PipeOperator::OrderBy { exprs } => { + write!(f, "ORDER BY {}", display_comma_separated(exprs.as_slice())) + } + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 28d479f3..661cd03d 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -98,10 +98,11 @@ impl Spanned for Query { order_by, limit_clause, fetch, - locks: _, // todo - for_clause: _, // todo, mssql specific - settings: _, // todo, clickhouse specific - format_clause: _, // todo, clickhouse specific + locks: _, // todo + for_clause: _, // todo, mssql specific + settings: _, // todo, clickhouse specific + format_clause: _, // todo, clickhouse specific + pipe_operators: _, // todo bigquery specific } = self; union_spans( diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 49fb24f1..68ca1390 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -136,6 +136,10 @@ impl Dialect for BigQueryDialect { fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { !RESERVED_FOR_COLUMN_ALIAS.contains(kw) } + + fn supports_pipe_operator(&self) -> bool { + true + } } impl BigQueryDialect { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b2dff065..b754a04f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -518,6 +518,20 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports pipe operator. + /// + /// Example: + /// ```sql + /// SELECT * + /// FROM table + /// |> limit 1 + /// ``` + /// + /// See + fn supports_pipe_operator(&self) -> bool { + false + } + /// Does the dialect support MySQL-style `'user'@'host'` grantee syntax? fn supports_user_host_grantee(&self) -> bool { false diff --git a/src/keywords.rs b/src/keywords.rs index 15a6f91a..d2ccbb2c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -83,6 +83,7 @@ define_keywords!( ADMIN, AFTER, AGAINST, + AGGREGATE, AGGREGATION, ALERT, ALGORITHM, @@ -338,6 +339,7 @@ define_keywords!( EXPLAIN, EXPLICIT, EXPORT, + EXTEND, EXTENDED, EXTENSION, EXTERNAL, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 03ea91fa..2e37f1bc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1149,6 +1149,25 @@ impl<'a> Parser<'a> { self.parse_subexpr(self.dialect.prec_unknown()) } + pub fn parse_expr_with_alias_and_order_by( + &mut self, + ) -> Result { + let expr = self.parse_expr()?; + + fn validator(explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { + explicit || !&[Keyword::ASC, Keyword::DESC, Keyword::GROUP].contains(kw) + } + let alias = self.parse_optional_alias_inner(None, validator)?; + let order_by = OrderByOptions { + asc: self.parse_asc_desc(), + nulls_first: None, + }; + Ok(ExprWithAliasAndOrderBy { + expr: ExprWithAlias { expr, alias }, + order_by, + }) + } + /// Parse tokens until the precedence changes. pub fn parse_subexpr(&mut self, precedence: u8) -> Result { let _guard = self.recursion_counter.try_decrease()?; @@ -10571,6 +10590,7 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], } .into()) } else if self.parse_keyword(Keyword::UPDATE) { @@ -10584,6 +10604,7 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], } .into()) } else if self.parse_keyword(Keyword::DELETE) { @@ -10597,6 +10618,7 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], } .into()) } else { @@ -10637,6 +10659,12 @@ impl<'a> Parser<'a> { None }; + let pipe_operators = if self.dialect.supports_pipe_operator() { + self.parse_pipe_operators()? + } else { + Vec::new() + }; + Ok(Query { with, body, @@ -10647,11 +10675,98 @@ impl<'a> Parser<'a> { for_clause, settings, format_clause, + pipe_operators, } .into()) } } + fn parse_pipe_operators(&mut self) -> Result, ParserError> { + let mut pipe_operators = Vec::new(); + + while self.consume_token(&Token::VerticalBarRightAngleBracket) { + let kw = self.expect_one_of_keywords(&[ + Keyword::SELECT, + Keyword::EXTEND, + Keyword::SET, + Keyword::DROP, + Keyword::AS, + Keyword::WHERE, + Keyword::LIMIT, + Keyword::AGGREGATE, + Keyword::ORDER, + ])?; + match kw { + Keyword::SELECT => { + let exprs = self.parse_comma_separated(Parser::parse_select_item)?; + pipe_operators.push(PipeOperator::Select { exprs }) + } + Keyword::EXTEND => { + let exprs = self.parse_comma_separated(Parser::parse_select_item)?; + pipe_operators.push(PipeOperator::Extend { exprs }) + } + Keyword::SET => { + let assignments = self.parse_comma_separated(Parser::parse_assignment)?; + pipe_operators.push(PipeOperator::Set { assignments }) + } + Keyword::DROP => { + let columns = self.parse_identifiers()?; + pipe_operators.push(PipeOperator::Drop { columns }) + } + Keyword::AS => { + let alias = self.parse_identifier()?; + pipe_operators.push(PipeOperator::As { alias }) + } + Keyword::WHERE => { + let expr = self.parse_expr()?; + pipe_operators.push(PipeOperator::Where { expr }) + } + Keyword::LIMIT => { + let expr = self.parse_expr()?; + let offset = if self.parse_keyword(Keyword::OFFSET) { + Some(self.parse_expr()?) + } else { + None + }; + pipe_operators.push(PipeOperator::Limit { expr, offset }) + } + Keyword::AGGREGATE => { + let full_table_exprs = if self.peek_keyword(Keyword::GROUP) { + vec![] + } else { + self.parse_comma_separated(|parser| { + parser.parse_expr_with_alias_and_order_by() + })? + }; + + let group_by_expr = if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { + self.parse_comma_separated(|parser| { + parser.parse_expr_with_alias_and_order_by() + })? + } else { + vec![] + }; + + pipe_operators.push(PipeOperator::Aggregate { + full_table_exprs, + group_by_expr, + }) + } + Keyword::ORDER => { + self.expect_one_of_keywords(&[Keyword::BY])?; + let exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; + pipe_operators.push(PipeOperator::OrderBy { exprs }) + } + unhandled => { + return Err(ParserError::ParserError(format!( + "`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}" + ))) + } + } + } + Ok(pipe_operators) + } + fn parse_settings(&mut self) -> Result>, ParserError> { let settings = if dialect_of!(self is ClickHouseDialect|GenericDialect) && self.parse_keyword(Keyword::SETTINGS) @@ -12122,6 +12237,7 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias, }) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 13bce0c0..4fad5462 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -246,6 +246,8 @@ pub enum Token { ShiftLeftVerticalBar, /// `|>> PostgreSQL/Redshift geometrical binary operator (Is strictly above?) VerticalBarShiftRight, + /// `|> BigQuery pipe operator + VerticalBarRightAngleBracket, /// `#>>`, extracts JSON sub-object at the specified path as text HashLongArrow, /// jsonb @> jsonb -> boolean: Test whether left json contains the right json @@ -359,6 +361,7 @@ impl fmt::Display for Token { Token::AmpersandRightAngleBracket => f.write_str("&>"), Token::AmpersandLeftAngleBracketVerticalBar => f.write_str("&<|"), Token::VerticalBarAmpersandRightAngleBracket => f.write_str("|&>"), + Token::VerticalBarRightAngleBracket => f.write_str("|>"), Token::TwoWayArrow => f.write_str("<->"), Token::LeftAngleBracketCaret => f.write_str("<^"), Token::RightAngleBracketCaret => f.write_str(">^"), @@ -1403,6 +1406,9 @@ impl<'a> Tokenizer<'a> { _ => self.start_binop_opt(chars, "|>", None), } } + Some('>') if self.dialect.supports_pipe_operator() => { + self.consume_for_binop(chars, "|>", Token::VerticalBarRightAngleBracket) + } // Bitshift '|' operator _ => self.start_binop(chars, "|", Token::Pipe), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fa2346c2..6d99929d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -489,6 +489,7 @@ fn parse_update_set_from() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias: Some(TableAlias { name: Ident::new("t2"), @@ -4310,6 +4311,7 @@ fn parse_create_table_as_table() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }); match verified_stmt(sql1) { @@ -4335,6 +4337,7 @@ fn parse_create_table_as_table() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }); match verified_stmt(sql2) { @@ -6332,6 +6335,7 @@ fn parse_interval_and_or_xor() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }))]; assert_eq!(actual_ast, expected_ast); @@ -9467,6 +9471,7 @@ fn parse_merge() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias: Some(TableAlias { name: Ident { @@ -11344,6 +11349,7 @@ fn parse_unload() { order_by: None, settings: None, format_clause: None, + pipe_operators: vec![], }), to: Ident { value: "s3://...".to_string(), @@ -12564,6 +12570,7 @@ fn test_extract_seconds_ok() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }))]; assert_eq!(actual_ast, expected_ast); @@ -14641,6 +14648,7 @@ fn test_select_from_first() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }; assert_eq!(expected, ast); assert_eq!(ast.to_string(), q); @@ -15020,6 +15028,82 @@ fn parse_set_names() { dialects.verified_stmt("SET NAMES UTF8 COLLATE bogus"); } +#[test] +fn parse_pipeline_operator() { + let dialects = all_dialects_where(|d| d.supports_pipe_operator()); + + // select pipe operator + dialects.verified_stmt("SELECT * FROM users |> SELECT id"); + dialects.verified_stmt("SELECT * FROM users |> SELECT id, name"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> SELECT id user_id", + "SELECT * FROM users |> SELECT id AS user_id", + ); + dialects.verified_stmt("SELECT * FROM users |> SELECT id AS user_id"); + + // extend pipe operator + dialects.verified_stmt("SELECT * FROM users |> EXTEND id + 1 AS new_id"); + dialects.verified_stmt("SELECT * FROM users |> EXTEND id AS new_id, name AS new_name"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> EXTEND id user_id", + "SELECT * FROM users |> EXTEND id AS user_id", + ); + + // set pipe operator + dialects.verified_stmt("SELECT * FROM users |> SET id = id + 1"); + dialects.verified_stmt("SELECT * FROM users |> SET id = id + 1, name = name + ' Doe'"); + + // drop pipe operator + dialects.verified_stmt("SELECT * FROM users |> DROP id"); + dialects.verified_stmt("SELECT * FROM users |> DROP id, name"); + + // as pipe operator + dialects.verified_stmt("SELECT * FROM users |> AS new_users"); + + // limit pipe operator + dialects.verified_stmt("SELECT * FROM users |> LIMIT 10"); + dialects.verified_stmt("SELECT * FROM users |> LIMIT 10 OFFSET 5"); + dialects.verified_stmt("SELECT * FROM users |> LIMIT 10 |> LIMIT 5"); + dialects.verified_stmt("SELECT * FROM users |> LIMIT 10 |> WHERE true"); + + // where pipe operator + dialects.verified_stmt("SELECT * FROM users |> WHERE id = 1"); + dialects.verified_stmt("SELECT * FROM users |> WHERE id = 1 AND name = 'John'"); + dialects.verified_stmt("SELECT * FROM users |> WHERE id = 1 OR name = 'John'"); + + // aggregate pipe operator full table + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE COUNT(*)"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> AGGREGATE COUNT(*) total_users", + "SELECT * FROM users |> AGGREGATE COUNT(*) AS total_users", + ); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE COUNT(*) AS total_users"); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE COUNT(*), MIN(id)"); + + // aggregate pipe opeprator with grouping + dialects.verified_stmt( + "SELECT * FROM users |> AGGREGATE SUM(o_totalprice) AS price, COUNT(*) AS cnt GROUP BY EXTRACT(YEAR FROM o_orderdate) AS year", + ); + dialects.verified_stmt( + "SELECT * FROM users |> AGGREGATE GROUP BY EXTRACT(YEAR FROM o_orderdate) AS year", + ); + dialects + .verified_stmt("SELECT * FROM users |> AGGREGATE GROUP BY EXTRACT(YEAR FROM o_orderdate)"); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE GROUP BY a, b"); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE SUM(c) GROUP BY a, b"); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE SUM(c) ASC"); + + // order by pipe operator + dialects.verified_stmt("SELECT * FROM users |> ORDER BY id ASC"); + dialects.verified_stmt("SELECT * FROM users |> ORDER BY id DESC"); + dialects.verified_stmt("SELECT * FROM users |> ORDER BY id DESC, name ASC"); + + // many pipes + dialects.verified_stmt( + "SELECT * FROM CustomerOrders |> AGGREGATE SUM(cost) AS total_cost GROUP BY customer_id, state, item_type |> EXTEND COUNT(*) OVER (PARTITION BY customer_id) AS num_orders |> WHERE num_orders > 1 |> AGGREGATE AVG(total_cost) AS average GROUP BY state DESC, item_type ASC", + ); +} + #[test] fn parse_multiple_set_statements() -> Result<(), ParserError> { let dialects = all_dialects_where(|d| d.supports_comma_separated_set_assignments()); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ef610347..b2d5210c 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -114,6 +114,7 @@ fn parse_create_procedure() { order_by: None, settings: None, format_clause: None, + pipe_operators: vec![], body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, @@ -1252,6 +1253,7 @@ fn parse_substring_in_select() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), query ); @@ -1354,6 +1356,8 @@ fn parse_mssql_declare() { order_by: None, settings: None, format_clause: None, + pipe_operators: vec![], + body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f74248b8..990107b2 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1113,6 +1113,7 @@ fn parse_escaped_quote_identifiers_with_escape() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1165,6 +1166,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1211,6 +1213,7 @@ fn parse_escaped_backticks_with_escape() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1261,6 +1264,7 @@ fn parse_escaped_backticks_with_no_escape() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1436,6 +1440,7 @@ fn parse_simple_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1484,6 +1489,7 @@ fn parse_ignore_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1532,6 +1538,7 @@ fn parse_priority_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1577,6 +1584,7 @@ fn parse_priority_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1624,6 +1632,7 @@ fn parse_insert_as() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1686,6 +1695,7 @@ fn parse_insert_as() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1735,6 +1745,7 @@ fn parse_replace_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1775,6 +1786,7 @@ fn parse_empty_row_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1839,6 +1851,7 @@ fn parse_insert_with_on_duplicate_update() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -2745,6 +2758,7 @@ fn parse_substring_in_select() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), query ); @@ -3051,6 +3065,7 @@ fn parse_hex_string_introducer() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 27fc7fa1..4ad8e00c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1326,6 +1326,7 @@ fn parse_copy_to() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), to: true, target: CopyTarget::File { @@ -2994,6 +2995,7 @@ fn parse_array_subquery_expr() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), filter: None, null_treatment: None, @@ -4785,6 +4787,7 @@ fn test_simple_postgres_insert_with_alias() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, @@ -4856,6 +4859,7 @@ fn test_simple_postgres_insert_with_alias() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, @@ -4925,6 +4929,7 @@ fn test_simple_insert_with_quoted_alias() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, From 483394cd1a29739ada14cb061da3f5cfc5e33506 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 2 May 2025 05:16:24 +0200 Subject: [PATCH 052/130] Added support for `DROP DOMAIN` (#1828) --- src/ast/mod.rs | 36 ++++++++++++++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 16 ++++++++++ tests/sqlparser_postgres.rs | 60 +++++++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b496403c..b1439266 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3321,6 +3321,14 @@ pub enum Statement { drop_behavior: Option, }, /// ```sql + /// DROP DOMAIN + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-dropdomain.html) + /// + /// DROP DOMAIN [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + /// + DropDomain(DropDomain), + /// ```sql /// DROP PROCEDURE /// ``` DropProcedure { @@ -5094,6 +5102,21 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DropDomain(DropDomain { + if_exists, + name, + drop_behavior, + }) => { + write!( + f, + "DROP DOMAIN{} {name}", + if *if_exists { " IF EXISTS" } else { "" }, + )?; + if let Some(op) = drop_behavior { + write!(f, " {op}")?; + } + Ok(()) + } Statement::DropProcedure { if_exists, proc_desc, @@ -6829,6 +6852,19 @@ impl fmt::Display for CloseCursor { } } +/// A Drop Domain statement +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DropDomain { + /// Whether to drop the domain if it exists + pub if_exists: bool, + /// The name of the domain to drop + pub name: ObjectName, + /// The behavior to apply when dropping the domain + pub drop_behavior: Option, +} + /// A function call #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 661cd03d..33bc0739 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -455,6 +455,7 @@ impl Spanned for Statement { Statement::DetachDuckDBDatabase { .. } => Span::empty(), Statement::Drop { .. } => Span::empty(), Statement::DropFunction { .. } => Span::empty(), + Statement::DropDomain { .. } => Span::empty(), Statement::DropProcedure { .. } => Span::empty(), Statement::DropSecret { .. } => Span::empty(), Statement::Declare { .. } => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index d2ccbb2c..32612ccd 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -288,6 +288,7 @@ define_keywords!( DISTRIBUTE, DIV, DO, + DOMAIN, DOUBLE, DOW, DOY, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2e37f1bc..0d74235b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6070,6 +6070,8 @@ impl<'a> Parser<'a> { return self.parse_drop_policy(); } else if self.parse_keyword(Keyword::CONNECTOR) { return self.parse_drop_connector(); + } else if self.parse_keyword(Keyword::DOMAIN) { + return self.parse_drop_domain(); } else if self.parse_keyword(Keyword::PROCEDURE) { return self.parse_drop_procedure(); } else if self.parse_keyword(Keyword::SECRET) { @@ -6165,6 +6167,20 @@ impl<'a> Parser<'a> { Ok(Statement::DropConnector { if_exists, name }) } + /// ```sql + /// DROP DOMAIN [ IF EXISTS ] name [ CASCADE | RESTRICT ] + /// ``` + fn parse_drop_domain(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let drop_behavior = self.parse_optional_drop_behavior(); + Ok(Statement::DropDomain(DropDomain { + if_exists, + name, + drop_behavior, + })) + } + /// ```sql /// DROP PROCEDURE [ IF EXISTS ] name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] [, ...] /// [ CASCADE | RESTRICT ] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 4ad8e00c..6c008c84 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4241,6 +4241,66 @@ fn parse_drop_function() { ); } +#[test] +fn parse_drop_domain() { + let sql = "DROP DOMAIN IF EXISTS jpeg_domain"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropDomain(DropDomain { + if_exists: true, + name: ObjectName::from(vec![Ident { + value: "jpeg_domain".to_string(), + quote_style: None, + span: Span::empty(), + }]), + drop_behavior: None + }) + ); + + let sql = "DROP DOMAIN jpeg_domain"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropDomain(DropDomain { + if_exists: false, + name: ObjectName::from(vec![Ident { + value: "jpeg_domain".to_string(), + quote_style: None, + span: Span::empty(), + }]), + drop_behavior: None + }) + ); + + let sql = "DROP DOMAIN IF EXISTS jpeg_domain CASCADE"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropDomain(DropDomain { + if_exists: true, + name: ObjectName::from(vec![Ident { + value: "jpeg_domain".to_string(), + quote_style: None, + span: Span::empty(), + }]), + drop_behavior: Some(DropBehavior::Cascade) + }) + ); + + let sql = "DROP DOMAIN IF EXISTS jpeg_domain RESTRICT"; + + assert_eq!( + pg().verified_stmt(sql), + Statement::DropDomain(DropDomain { + if_exists: true, + name: ObjectName::from(vec![Ident { + value: "jpeg_domain".to_string(), + quote_style: None, + span: Span::empty(), + }]), + drop_behavior: Some(DropBehavior::Restrict) + }) + ); +} + #[test] fn parse_drop_procedure() { let sql = "DROP PROCEDURE IF EXISTS test_proc"; From a464f8e8d7a5057e4e5b8046b0f619acdf7fce74 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 1 May 2025 23:25:30 -0400 Subject: [PATCH 053/130] Improve support for cursors for SQL Server (#1831) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 90 +++++++++++++++++++++++++++++++++++++-- src/ast/spans.rs | 31 +++++++++++--- src/keywords.rs | 2 + src/parser/mod.rs | 70 +++++++++++++++++++++++++++--- src/test_utils.rs | 20 +++++++++ tests/sqlparser_common.rs | 12 ++++++ tests/sqlparser_mssql.rs | 84 +++++++++++++++++++++++++++++++++++- 7 files changed, 289 insertions(+), 20 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b1439266..582922a3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2228,7 +2228,33 @@ impl fmt::Display for IfStatement { } } -/// A block within a [Statement::Case] or [Statement::If]-like statement +/// A `WHILE` statement. +/// +/// Example: +/// ```sql +/// WHILE @@FETCH_STATUS = 0 +/// BEGIN +/// FETCH NEXT FROM c1 INTO @var1, @var2; +/// END +/// ``` +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/while-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct WhileStatement { + pub while_block: ConditionalStatementBlock, +} + +impl fmt::Display for WhileStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let WhileStatement { while_block } = self; + write!(f, "{while_block}")?; + Ok(()) + } +} + +/// A block within a [Statement::Case] or [Statement::If] or [Statement::While]-like statement /// /// Example 1: /// ```sql @@ -2244,6 +2270,14 @@ impl fmt::Display for IfStatement { /// ```sql /// ELSE SELECT 1; SELECT 2; /// ``` +/// +/// Example 4: +/// ```sql +/// WHILE @@FETCH_STATUS = 0 +/// BEGIN +/// FETCH NEXT FROM c1 INTO @var1, @var2; +/// END +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2983,6 +3017,8 @@ pub enum Statement { Case(CaseStatement), /// An `IF` statement. If(IfStatement), + /// A `WHILE` statement. + While(WhileStatement), /// A `RAISE` statement. Raise(RaiseStatement), /// ```sql @@ -3034,6 +3070,11 @@ pub enum Statement { partition: Option>, }, /// ```sql + /// OPEN cursor_name + /// ``` + /// Opens a cursor. + Open(OpenStatement), + /// ```sql /// CLOSE /// ``` /// Closes the portal underlying an open cursor. @@ -3413,6 +3454,7 @@ pub enum Statement { /// Cursor name name: Ident, direction: FetchDirection, + position: FetchPosition, /// Optional, It's possible to fetch rows form cursor to the table into: Option, }, @@ -4235,11 +4277,10 @@ impl fmt::Display for Statement { Statement::Fetch { name, direction, + position, into, } => { - write!(f, "FETCH {direction} ")?; - - write!(f, "IN {name}")?; + write!(f, "FETCH {direction} {position} {name}")?; if let Some(into) = into { write!(f, " INTO {into}")?; @@ -4329,6 +4370,9 @@ impl fmt::Display for Statement { Statement::If(stmt) => { write!(f, "{stmt}") } + Statement::While(stmt) => { + write!(f, "{stmt}") + } Statement::Raise(stmt) => { write!(f, "{stmt}") } @@ -4498,6 +4542,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::Delete(delete) => write!(f, "{delete}"), + Statement::Open(open) => write!(f, "{open}"), Statement::Close { cursor } => { write!(f, "CLOSE {cursor}")?; @@ -6187,6 +6232,28 @@ impl fmt::Display for FetchDirection { } } +/// The "position" for a FETCH statement. +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/fetch-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FetchPosition { + From, + In, +} + +impl fmt::Display for FetchPosition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FetchPosition::From => f.write_str("FROM")?, + FetchPosition::In => f.write_str("IN")?, + }; + + Ok(()) + } +} + /// A privilege on a database object (table, sequence, etc.). #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -9354,6 +9421,21 @@ pub enum ReturnStatementValue { Expr(Expr), } +/// Represents an `OPEN` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct OpenStatement { + /// Cursor name + pub cursor_name: Ident, +} + +impl fmt::Display for OpenStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "OPEN {}", self.cursor_name) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 33bc0739..836f229a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -31,13 +31,13 @@ use super::{ FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, - Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, - PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, - ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, - SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, - TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, - TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, - WildcardAdditionalOptions, With, WithFill, + Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, OrderByExpr, + OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, RaiseStatement, + RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, + SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, + TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, + WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -339,6 +339,7 @@ impl Spanned for Statement { } => source.span(), Statement::Case(stmt) => stmt.span(), Statement::If(stmt) => stmt.span(), + Statement::While(stmt) => stmt.span(), Statement::Raise(stmt) => stmt.span(), Statement::Call(function) => function.span(), Statement::Copy { @@ -365,6 +366,7 @@ impl Spanned for Statement { from_query: _, partition: _, } => Span::empty(), + Statement::Open(open) => open.span(), Statement::Close { cursor } => match cursor { CloseCursor::All => Span::empty(), CloseCursor::Specific { name } => name.span, @@ -776,6 +778,14 @@ impl Spanned for IfStatement { } } +impl Spanned for WhileStatement { + fn span(&self) -> Span { + let WhileStatement { while_block } = self; + + while_block.span() + } +} + impl Spanned for ConditionalStatements { fn span(&self) -> Span { match self { @@ -2297,6 +2307,13 @@ impl Spanned for BeginEndStatements { } } +impl Spanned for OpenStatement { + fn span(&self) -> Span { + let OpenStatement { cursor_name } = self; + cursor_name.span + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; diff --git a/src/keywords.rs b/src/keywords.rs index 32612ccd..bf8a1915 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -985,6 +985,7 @@ define_keywords!( WHEN, WHENEVER, WHERE, + WHILE, WIDTH_BUCKET, WINDOW, WITH, @@ -1068,6 +1069,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::SAMPLE, Keyword::TABLESAMPLE, Keyword::FROM, + Keyword::OPEN, ]; /// Can't be used as a column alias, so that `SELECT alias` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0d74235b..cbd464c3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -536,6 +536,10 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_if_stmt() } + Keyword::WHILE => { + self.prev_token(); + self.parse_while() + } Keyword::RAISE => { self.prev_token(); self.parse_raise_stmt() @@ -570,6 +574,10 @@ impl<'a> Parser<'a> { Keyword::ALTER => self.parse_alter(), Keyword::CALL => self.parse_call(), Keyword::COPY => self.parse_copy(), + Keyword::OPEN => { + self.prev_token(); + self.parse_open() + } Keyword::CLOSE => self.parse_close(), Keyword::SET => self.parse_set(), Keyword::SHOW => self.parse_show(), @@ -700,8 +708,18 @@ impl<'a> Parser<'a> { })) } + /// Parse a `WHILE` statement. + /// + /// See [Statement::While] + fn parse_while(&mut self) -> Result { + self.expect_keyword_is(Keyword::WHILE)?; + let while_block = self.parse_conditional_statement_block(&[Keyword::END])?; + + Ok(Statement::While(WhileStatement { while_block })) + } + /// Parses an expression and associated list of statements - /// belonging to a conditional statement like `IF` or `WHEN`. + /// belonging to a conditional statement like `IF` or `WHEN` or `WHILE`. /// /// Example: /// ```sql @@ -716,6 +734,10 @@ impl<'a> Parser<'a> { let condition = match &start_token.token { Token::Word(w) if w.keyword == Keyword::ELSE => None, + Token::Word(w) if w.keyword == Keyword::WHILE => { + let expr = self.parse_expr()?; + Some(expr) + } _ => { let expr = self.parse_expr()?; then_token = Some(AttachedToken(self.expect_keyword(Keyword::THEN)?)); @@ -723,13 +745,25 @@ impl<'a> Parser<'a> { } }; - let statements = self.parse_statement_list(terminal_keywords)?; + let conditional_statements = if self.peek_keyword(Keyword::BEGIN) { + let begin_token = self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(terminal_keywords)?; + let end_token = self.expect_keyword(Keyword::END)?; + ConditionalStatements::BeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + }) + } else { + let statements = self.parse_statement_list(terminal_keywords)?; + ConditionalStatements::Sequence { statements } + }; Ok(ConditionalStatementBlock { start_token: AttachedToken(start_token), condition, then_token, - conditional_statements: ConditionalStatements::Sequence { statements }, + conditional_statements, }) } @@ -4467,11 +4501,16 @@ impl<'a> Parser<'a> { ) -> Result, ParserError> { let mut values = vec![]; loop { - if let Token::Word(w) = &self.peek_nth_token_ref(0).token { - if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { - break; + match &self.peek_nth_token_ref(0).token { + Token::EOF => break, + Token::Word(w) => { + if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { + break; + } } + _ => {} } + values.push(self.parse_statement()?); self.expect_token(&Token::SemiColon)?; } @@ -6644,7 +6683,15 @@ impl<'a> Parser<'a> { } }; - self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; + let position = if self.peek_keyword(Keyword::FROM) { + self.expect_keyword(Keyword::FROM)?; + FetchPosition::From + } else if self.peek_keyword(Keyword::IN) { + self.expect_keyword(Keyword::IN)?; + FetchPosition::In + } else { + return parser_err!("Expected FROM or IN", self.peek_token().span.start); + }; let name = self.parse_identifier()?; @@ -6657,6 +6704,7 @@ impl<'a> Parser<'a> { Ok(Statement::Fetch { name, direction, + position, into, }) } @@ -8770,6 +8818,14 @@ impl<'a> Parser<'a> { }) } + /// Parse [Statement::Open] + fn parse_open(&mut self) -> Result { + self.expect_keyword(Keyword::OPEN)?; + Ok(Statement::Open(OpenStatement { + cursor_name: self.parse_identifier()?, + })) + } + pub fn parse_close(&mut self) -> Result { let cursor = if self.parse_keyword(Keyword::ALL) { CloseCursor::All diff --git a/src/test_utils.rs b/src/test_utils.rs index 6270ac42..3c22fa91 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -151,6 +151,8 @@ impl TestedDialects { /// /// 2. re-serializing the result of parsing `sql` produces the same /// `canonical` sql string + /// + /// For multiple statements, use [`statements_parse_to`]. pub fn one_statement_parses_to(&self, sql: &str, canonical: &str) -> Statement { let mut statements = self.parse_sql_statements(sql).expect(sql); assert_eq!(statements.len(), 1); @@ -166,6 +168,24 @@ impl TestedDialects { only_statement } + /// The same as [`one_statement_parses_to`] but it works for a multiple statements + pub fn statements_parse_to(&self, sql: &str, canonical: &str) -> Vec { + let statements = self.parse_sql_statements(sql).expect(sql); + if !canonical.is_empty() && sql != canonical { + assert_eq!(self.parse_sql_statements(canonical).unwrap(), statements); + } else { + assert_eq!( + sql, + statements + .iter() + .map(|s| s.to_string()) + .collect::>() + .join("; ") + ); + } + statements + } + /// Ensures that `sql` parses as an [`Expr`], and that /// re-serializing the parse result produces canonical pub fn expr_parses_to(&self, sql: &str, canonical: &str) -> Expr { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6d99929d..1ddf3f92 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15187,3 +15187,15 @@ fn parse_return() { let _ = all_dialects().verified_stmt("RETURN 1"); } + +#[test] +fn test_open() { + let open_cursor = "OPEN Employee_Cursor"; + let stmt = all_dialects().verified_stmt(open_cursor); + assert_eq!( + stmt, + Statement::Open(OpenStatement { + cursor_name: Ident::new("Employee_Cursor"), + }) + ); +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b2d5210c..88e7a1f1 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -23,7 +23,8 @@ mod test_utils; use helpers::attached_token::AttachedToken; -use sqlparser::tokenizer::{Location, Span}; +use sqlparser::keywords::Keyword; +use sqlparser::tokenizer::{Location, Span, Token, TokenWithSpan, Word}; use test_utils::*; use sqlparser::ast::DataType::{Int, Text, Varbinary}; @@ -223,7 +224,7 @@ fn parse_create_function() { value: Some(ReturnStatementValue::Expr(Expr::Value( (number("1")).with_empty_span() ))), - }),], + })], end_token: AttachedToken::empty(), })), behavior: None, @@ -1397,6 +1398,85 @@ fn parse_mssql_declare() { let _ = ms().verified_stmt(declare_cursor_for_select); } +#[test] +fn test_mssql_cursor() { + let full_cursor_usage = "\ + DECLARE Employee_Cursor CURSOR FOR \ + SELECT LastName, FirstName \ + FROM AdventureWorks2022.HumanResources.vEmployee \ + WHERE LastName LIKE 'B%'; \ + \ + OPEN Employee_Cursor; \ + \ + FETCH NEXT FROM Employee_Cursor; \ + \ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + END; \ + \ + CLOSE Employee_Cursor; \ + DEALLOCATE Employee_Cursor\ + "; + let _ = ms().statements_parse_to(full_cursor_usage, ""); +} + +#[test] +fn test_mssql_while_statement() { + let while_single_statement = "WHILE 1 = 0 PRINT 'Hello World';"; + let stmt = ms().verified_stmt(while_single_statement); + assert_eq!( + stmt, + Statement::While(sqlparser::ast::WhileStatement { + while_block: ConditionalStatementBlock { + start_token: AttachedToken(TokenWithSpan { + token: Token::Word(Word { + value: "WHILE".to_string(), + quote_style: None, + keyword: Keyword::WHILE + }), + span: Span::empty() + }), + condition: Some(Expr::BinaryOp { + left: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value( + (Value::Number("0".parse().unwrap(), false)).with_empty_span() + )), + }), + then_token: None, + conditional_statements: ConditionalStatements::Sequence { + statements: vec![Statement::Print(PrintStatement { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("Hello World".to_string())) + .with_empty_span() + )), + })], + } + } + }) + ); + + let while_begin_end = "\ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + END\ + "; + let _ = ms().verified_stmt(while_begin_end); + + let while_begin_end_multiple_statements = "\ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + PRINT 'Hello World'; \ + END\ + "; + let _ = ms().verified_stmt(while_begin_end_multiple_statements); +} + #[test] fn test_parse_raiserror() { let sql = r#"RAISERROR('This is a test', 16, 1)"#; From 728645fb31f8e41640255c955cfe90e7f98e7752 Mon Sep 17 00:00:00 2001 From: benrsatori Date: Fri, 2 May 2025 16:16:59 +0300 Subject: [PATCH 054/130] Add all missing table options to be handled in any order (#1747) Co-authored-by: Tomer Shani --- src/ast/dml.rs | 75 ++---- src/ast/helpers/stmt_create_table.rs | 91 ++------ src/ast/helpers/stmt_data_loading.rs | 2 - src/ast/mod.rs | 111 +++++++-- src/ast/spans.rs | 39 ++-- src/dialect/snowflake.rs | 17 +- src/keywords.rs | 17 ++ src/parser/mod.rs | 334 ++++++++++++++++++++------- tests/sqlparser_bigquery.rs | 6 +- tests/sqlparser_clickhouse.rs | 37 +-- tests/sqlparser_common.rs | 47 ++-- tests/sqlparser_duckdb.rs | 8 +- tests/sqlparser_hive.rs | 4 +- tests/sqlparser_mssql.rs | 18 +- tests/sqlparser_mysql.rs | 304 +++++++++++++++++++++--- tests/sqlparser_postgres.rs | 22 +- tests/sqlparser_snowflake.rs | 17 +- 17 files changed, 767 insertions(+), 382 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 9cdb1ca8..7ed17be9 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -33,11 +33,11 @@ pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ display_comma_separated, display_separated, query::InputFormatClause, Assignment, ClusteredBy, - CommentDef, Expr, FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, - HiveRowFormat, Ident, IndexType, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, - OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, Setting, - SqlOption, SqliteOnConflict, StorageSerializationPolicy, TableEngine, TableObject, - TableWithJoins, Tag, WrappedCollection, + CommentDef, CreateTableOptions, Expr, FileFormat, FromTable, HiveDistributionStyle, HiveFormat, + HiveIOFormat, HiveRowFormat, Ident, IndexType, InsertAliases, MysqlInsertPriority, ObjectName, + OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, + Setting, SqliteOnConflict, StorageSerializationPolicy, TableObject, TableWithJoins, Tag, + WrappedCollection, }; /// Index column type. @@ -146,19 +146,17 @@ pub struct CreateTable { pub constraints: Vec, pub hive_distribution: HiveDistributionStyle, pub hive_formats: Option, - pub table_properties: Vec, - pub with_options: Vec, + pub table_options: CreateTableOptions, pub file_format: Option, pub location: Option, pub query: Option>, pub without_rowid: bool, pub like: Option, pub clone: Option, - pub engine: Option, + // For Hive dialect, the table comment is after the column definitions without `=`, + // so the `comment` field is optional and different than the comment field in the general options list. + // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) pub comment: Option, - pub auto_increment_offset: Option, - pub default_charset: Option, - pub collation: Option, pub on_commit: Option, /// ClickHouse "ON CLUSTER" clause: /// @@ -179,9 +177,6 @@ pub struct CreateTable { /// Hive: Table clustering column list. /// pub clustered_by: Option, - /// BigQuery: Table options list. - /// - pub options: Option>, /// Postgres `INHERITs` clause, which contains the list of tables from which /// the new table inherits. /// @@ -282,7 +277,7 @@ impl Display for CreateTable { // Hive table comment should be after column definitions, please refer to: // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) - if let Some(CommentDef::AfterColumnDefsWithoutEq(comment)) = &self.comment { + if let Some(comment) = &self.comment { write!(f, " COMMENT '{comment}'")?; } @@ -375,35 +370,14 @@ impl Display for CreateTable { } write!(f, " LOCATION '{}'", self.location.as_ref().unwrap())?; } - if !self.table_properties.is_empty() { - write!( - f, - " TBLPROPERTIES ({})", - display_comma_separated(&self.table_properties) - )?; - } - if !self.with_options.is_empty() { - write!(f, " WITH ({})", display_comma_separated(&self.with_options))?; - } - if let Some(engine) = &self.engine { - write!(f, " ENGINE={engine}")?; - } - if let Some(comment_def) = &self.comment { - match comment_def { - CommentDef::WithEq(comment) => { - write!(f, " COMMENT = '{comment}'")?; - } - CommentDef::WithoutEq(comment) => { - write!(f, " COMMENT '{comment}'")?; - } - // For CommentDef::AfterColumnDefsWithoutEq will be displayed after column definition - CommentDef::AfterColumnDefsWithoutEq(_) => (), - } + + match &self.table_options { + options @ CreateTableOptions::With(_) + | options @ CreateTableOptions::Plain(_) + | options @ CreateTableOptions::TableProperties(_) => write!(f, " {}", options)?, + _ => (), } - if let Some(auto_increment_offset) = self.auto_increment_offset { - write!(f, " AUTO_INCREMENT {auto_increment_offset}")?; - } if let Some(primary_key) = &self.primary_key { write!(f, " PRIMARY KEY {}", primary_key)?; } @@ -419,15 +393,9 @@ impl Display for CreateTable { if let Some(cluster_by) = self.cluster_by.as_ref() { write!(f, " CLUSTER BY {cluster_by}")?; } - - if let Some(options) = self.options.as_ref() { - write!( - f, - " OPTIONS({})", - display_comma_separated(options.as_slice()) - )?; + if let options @ CreateTableOptions::Options(_) = &self.table_options { + write!(f, " {}", options)?; } - if let Some(external_volume) = self.external_volume.as_ref() { write!(f, " EXTERNAL_VOLUME = '{external_volume}'")?; } @@ -503,13 +471,6 @@ impl Display for CreateTable { write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; } - if let Some(default_charset) = &self.default_charset { - write!(f, " DEFAULT CHARSET={default_charset}")?; - } - if let Some(collation) = &self.collation { - write!(f, " COLLATE={collation}")?; - } - if self.on_commit.is_some() { let on_commit = match self.on_commit { Some(OnCommit::DeleteRows) => "ON COMMIT DELETE ROWS", diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 1c50cb84..542d30ea 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -26,10 +26,12 @@ use sqlparser_derive::{Visit, VisitMut}; use super::super::dml::CreateTable; use crate::ast::{ - ClusteredBy, ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, - ObjectName, OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, - StorageSerializationPolicy, TableConstraint, TableEngine, Tag, WrappedCollection, + ClusteredBy, ColumnDef, CommentDef, CreateTableOptions, Expr, FileFormat, + HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, OneOrManyWithParens, Query, + RowAccessPolicy, Statement, StorageSerializationPolicy, TableConstraint, Tag, + WrappedCollection, }; + use crate::parser::ParserError; /// Builder for create table statement variant ([1]). @@ -76,19 +78,13 @@ pub struct CreateTableBuilder { pub constraints: Vec, pub hive_distribution: HiveDistributionStyle, pub hive_formats: Option, - pub table_properties: Vec, - pub with_options: Vec, pub file_format: Option, pub location: Option, pub query: Option>, pub without_rowid: bool, pub like: Option, pub clone: Option, - pub engine: Option, pub comment: Option, - pub auto_increment_offset: Option, - pub default_charset: Option, - pub collation: Option, pub on_commit: Option, pub on_cluster: Option, pub primary_key: Option>, @@ -96,7 +92,6 @@ pub struct CreateTableBuilder { pub partition_by: Option>, pub cluster_by: Option>>, pub clustered_by: Option, - pub options: Option>, pub inherits: Option>, pub strict: bool, pub copy_grants: bool, @@ -113,6 +108,7 @@ pub struct CreateTableBuilder { pub catalog: Option, pub catalog_sync: Option, pub storage_serialization_policy: Option, + pub table_options: CreateTableOptions, } impl CreateTableBuilder { @@ -131,19 +127,13 @@ impl CreateTableBuilder { constraints: vec![], hive_distribution: HiveDistributionStyle::NONE, hive_formats: None, - table_properties: vec![], - with_options: vec![], file_format: None, location: None, query: None, without_rowid: false, like: None, clone: None, - engine: None, comment: None, - auto_increment_offset: None, - default_charset: None, - collation: None, on_commit: None, on_cluster: None, primary_key: None, @@ -151,7 +141,6 @@ impl CreateTableBuilder { partition_by: None, cluster_by: None, clustered_by: None, - options: None, inherits: None, strict: false, copy_grants: false, @@ -168,6 +157,7 @@ impl CreateTableBuilder { catalog: None, catalog_sync: None, storage_serialization_policy: None, + table_options: CreateTableOptions::None, } } pub fn or_replace(mut self, or_replace: bool) -> Self { @@ -230,15 +220,6 @@ impl CreateTableBuilder { self } - pub fn table_properties(mut self, table_properties: Vec) -> Self { - self.table_properties = table_properties; - self - } - - pub fn with_options(mut self, with_options: Vec) -> Self { - self.with_options = with_options; - self - } pub fn file_format(mut self, file_format: Option) -> Self { self.file_format = file_format; self @@ -268,31 +249,11 @@ impl CreateTableBuilder { self } - pub fn engine(mut self, engine: Option) -> Self { - self.engine = engine; - self - } - - pub fn comment(mut self, comment: Option) -> Self { + pub fn comment_after_column_def(mut self, comment: Option) -> Self { self.comment = comment; self } - pub fn auto_increment_offset(mut self, offset: Option) -> Self { - self.auto_increment_offset = offset; - self - } - - pub fn default_charset(mut self, default_charset: Option) -> Self { - self.default_charset = default_charset; - self - } - - pub fn collation(mut self, collation: Option) -> Self { - self.collation = collation; - self - } - pub fn on_commit(mut self, on_commit: Option) -> Self { self.on_commit = on_commit; self @@ -328,11 +289,6 @@ impl CreateTableBuilder { self } - pub fn options(mut self, options: Option>) -> Self { - self.options = options; - self - } - pub fn inherits(mut self, inherits: Option>) -> Self { self.inherits = inherits; self @@ -422,6 +378,11 @@ impl CreateTableBuilder { self } + pub fn table_options(mut self, table_options: CreateTableOptions) -> Self { + self.table_options = table_options; + self + } + pub fn build(self) -> Statement { Statement::CreateTable(CreateTable { or_replace: self.or_replace, @@ -437,19 +398,13 @@ impl CreateTableBuilder { constraints: self.constraints, hive_distribution: self.hive_distribution, hive_formats: self.hive_formats, - table_properties: self.table_properties, - with_options: self.with_options, file_format: self.file_format, location: self.location, query: self.query, without_rowid: self.without_rowid, like: self.like, clone: self.clone, - engine: self.engine, comment: self.comment, - auto_increment_offset: self.auto_increment_offset, - default_charset: self.default_charset, - collation: self.collation, on_commit: self.on_commit, on_cluster: self.on_cluster, primary_key: self.primary_key, @@ -457,7 +412,6 @@ impl CreateTableBuilder { partition_by: self.partition_by, cluster_by: self.cluster_by, clustered_by: self.clustered_by, - options: self.options, inherits: self.inherits, strict: self.strict, copy_grants: self.copy_grants, @@ -474,6 +428,7 @@ impl CreateTableBuilder { catalog: self.catalog, catalog_sync: self.catalog_sync, storage_serialization_policy: self.storage_serialization_policy, + table_options: self.table_options, }) } } @@ -499,19 +454,13 @@ impl TryFrom for CreateTableBuilder { constraints, hive_distribution, hive_formats, - table_properties, - with_options, file_format, location, query, without_rowid, like, clone, - engine, comment, - auto_increment_offset, - default_charset, - collation, on_commit, on_cluster, primary_key, @@ -519,7 +468,6 @@ impl TryFrom for CreateTableBuilder { partition_by, cluster_by, clustered_by, - options, inherits, strict, copy_grants, @@ -536,6 +484,7 @@ impl TryFrom for CreateTableBuilder { catalog, catalog_sync, storage_serialization_policy, + table_options, }) => Ok(Self { or_replace, temporary, @@ -548,19 +497,13 @@ impl TryFrom for CreateTableBuilder { constraints, hive_distribution, hive_formats, - table_properties, - with_options, file_format, location, query, without_rowid, like, clone, - engine, comment, - auto_increment_offset, - default_charset, - collation, on_commit, on_cluster, primary_key, @@ -568,7 +511,6 @@ impl TryFrom for CreateTableBuilder { partition_by, cluster_by, clustered_by, - options, inherits, strict, iceberg, @@ -587,6 +529,7 @@ impl TryFrom for CreateTableBuilder { catalog, catalog_sync, storage_serialization_policy, + table_options, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" @@ -600,8 +543,8 @@ impl TryFrom for CreateTableBuilder { pub(crate) struct CreateTableConfiguration { pub partition_by: Option>, pub cluster_by: Option>>, - pub options: Option>, pub inherits: Option>, + pub table_options: CreateTableOptions, } #[cfg(test)] diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index e960bb05..92a72727 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -21,8 +21,6 @@ #[cfg(not(feature = "std"))] use alloc::string::String; -#[cfg(not(feature = "std"))] -use alloc::vec::Vec; use core::fmt; #[cfg(feature = "serde")] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 582922a3..d74d197e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2681,6 +2681,18 @@ pub enum CreateTableOptions { /// /// Options(Vec), + + /// Plain options, options which are not part on any declerative statement e.g. WITH/OPTIONS/... + /// + Plain(Vec), + + TableProperties(Vec), +} + +impl Default for CreateTableOptions { + fn default() -> Self { + Self::None + } } impl fmt::Display for CreateTableOptions { @@ -2692,6 +2704,12 @@ impl fmt::Display for CreateTableOptions { CreateTableOptions::Options(options) => { write!(f, "OPTIONS({})", display_comma_separated(options)) } + CreateTableOptions::TableProperties(options) => { + write!(f, "TBLPROPERTIES ({})", display_comma_separated(options)) + } + CreateTableOptions::Plain(options) => { + write!(f, "{}", display_separated(options, " ")) + } CreateTableOptions::None => Ok(()), } } @@ -7560,6 +7578,18 @@ pub enum SqlOption { range_direction: Option, for_values: Vec, }, + /// Comment parameter (supports `=` and no `=` syntax) + Comment(CommentDef), + /// MySQL TableSpace option + /// + TableSpace(TablespaceOption), + /// An option representing a key value pair, where the value is a parenthesized list and with an optional name + /// e.g. + /// + /// UNION = (tbl_name\[,tbl_name\]...) + /// ENGINE = ReplicatedMergeTree('/table_name','{replica}', ver) + /// ENGINE = SummingMergeTree(\[columns\]) + NamedParenthesizedList(NamedParenthesizedList), } impl fmt::Display for SqlOption { @@ -7591,10 +7621,54 @@ impl fmt::Display for SqlOption { display_comma_separated(for_values) ) } + SqlOption::TableSpace(tablespace_option) => { + write!(f, "TABLESPACE {}", tablespace_option.name)?; + match tablespace_option.storage { + Some(StorageType::Disk) => write!(f, " STORAGE DISK"), + Some(StorageType::Memory) => write!(f, " STORAGE MEMORY"), + None => Ok(()), + } + } + SqlOption::Comment(comment) => match comment { + CommentDef::WithEq(comment) => { + write!(f, "COMMENT = '{comment}'") + } + CommentDef::WithoutEq(comment) => { + write!(f, "COMMENT '{comment}'") + } + }, + SqlOption::NamedParenthesizedList(value) => { + write!(f, "{} = ", value.key)?; + if let Some(key) = &value.name { + write!(f, "{}", key)?; + } + if !value.values.is_empty() { + write!(f, "({})", display_comma_separated(&value.values))? + } + Ok(()) + } } } } +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum StorageType { + Disk, + Memory, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// MySql TableSpace option +/// +pub struct TablespaceOption { + pub name: String, + pub storage: Option, +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -8860,27 +8934,20 @@ impl Display for CreateViewParams { } } -/// Engine of DB. Some warehouse has parameters of engine, e.g. [ClickHouse] -/// -/// [ClickHouse]: https://clickhouse.com/docs/en/engines/table-engines #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct TableEngine { - pub name: String, - pub parameters: Option>, -} - -impl Display for TableEngine { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name)?; - - if let Some(parameters) = self.parameters.as_ref() { - write!(f, "({})", display_comma_separated(parameters))?; - } - - Ok(()) - } +/// Key/Value, where the value is a (optionally named) list of identifiers +/// +/// ```sql +/// UNION = (tbl_name[,tbl_name]...) +/// ENGINE = ReplicatedMergeTree('/table_name','{replica}', ver) +/// ENGINE = SummingMergeTree([columns]) +/// ``` +pub struct NamedParenthesizedList { + pub key: Ident, + pub name: Option, + pub values: Vec, } /// Snowflake `WITH ROW ACCESS POLICY policy_name ON (identifier, ...)` @@ -8944,18 +9011,12 @@ pub enum CommentDef { /// Does not include `=` when printing the comment, as `COMMENT 'comment'` WithEq(String), WithoutEq(String), - // For Hive dialect, the table comment is after the column definitions without `=`, - // so we need to add an extra variant to allow to identify this case when displaying. - // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) - AfterColumnDefsWithoutEq(String), } impl Display for CommentDef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - CommentDef::WithEq(comment) - | CommentDef::WithoutEq(comment) - | CommentDef::AfterColumnDefsWithoutEq(comment) => write!(f, "{comment}"), + CommentDef::WithEq(comment) | CommentDef::WithoutEq(comment) => write!(f, "{comment}"), } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 836f229a..3f703ffa 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -30,10 +30,10 @@ use super::{ Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, - Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, OrderByExpr, - OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, RaiseStatement, - RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, NamedWindowDefinition, + ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, + OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, + RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, @@ -567,27 +567,20 @@ impl Spanned for CreateTable { constraints, hive_distribution: _, // hive specific hive_formats: _, // hive specific - table_properties, - with_options, - file_format: _, // enum - location: _, // string, no span + file_format: _, // enum + location: _, // string, no span query, without_rowid: _, // bool like, clone, - engine: _, // todo - comment: _, // todo, no span - auto_increment_offset: _, // u32, no span - default_charset: _, // string, no span - collation: _, // string, no span - on_commit: _, // enum + comment: _, // todo, no span + on_commit: _, on_cluster: _, // todo, clickhouse specific primary_key: _, // todo, clickhouse specific order_by: _, // todo, clickhouse specific partition_by: _, // todo, BigQuery specific cluster_by: _, // todo, BigQuery specific clustered_by: _, // todo, Hive specific - options: _, // todo, BigQuery specific inherits: _, // todo, PostgreSQL specific strict: _, // bool copy_grants: _, // bool @@ -603,15 +596,15 @@ impl Spanned for CreateTable { base_location: _, // todo, Snowflake specific catalog: _, // todo, Snowflake specific catalog_sync: _, // todo, Snowflake specific - storage_serialization_policy: _, // todo, Snowflake specific + storage_serialization_policy: _, + table_options, } = self; union_spans( core::iter::once(name.span()) + .chain(core::iter::once(table_options.span())) .chain(columns.iter().map(|i| i.span())) .chain(constraints.iter().map(|i| i.span())) - .chain(table_properties.iter().map(|i| i.span())) - .chain(with_options.iter().map(|i| i.span())) .chain(query.iter().map(|i| i.span())) .chain(like.iter().map(|i| i.span())) .chain(clone.iter().map(|i| i.span())), @@ -1004,6 +997,14 @@ impl Spanned for SqlOption { } => union_spans( core::iter::once(column_name.span).chain(for_values.iter().map(|i| i.span())), ), + SqlOption::TableSpace(_) => Span::empty(), + SqlOption::Comment(_) => Span::empty(), + SqlOption::NamedParenthesizedList(NamedParenthesizedList { + key: name, + name: value, + values, + }) => union_spans(core::iter::once(name.span).chain(values.iter().map(|i| i.span))) + .union_opt(&value.as_ref().map(|i| i.span)), } } } @@ -1041,6 +1042,8 @@ impl Spanned for CreateTableOptions { CreateTableOptions::None => Span::empty(), CreateTableOptions::With(vec) => union_spans(vec.iter().map(|i| i.span())), CreateTableOptions::Options(vec) => union_spans(vec.iter().map(|i| i.span())), + CreateTableOptions::Plain(vec) => union_spans(vec.iter().map(|i| i.span())), + CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())), } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index c4d6a5ad..ccce1619 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -25,8 +25,8 @@ use crate::ast::helpers::stmt_data_loading::{ use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, Statement, TagsColumnOption, - WrappedCollection, + IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, SqlOption, Statement, + TagsColumnOption, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -417,6 +417,8 @@ pub fn parse_create_table( // "CREATE TABLE x COPY GRANTS (c INT)" and "CREATE TABLE x (c INT) COPY GRANTS" are both // accepted by Snowflake + let mut plain_options = vec![]; + loop { let next_token = parser.next_token(); match &next_token.token { @@ -428,7 +430,9 @@ pub fn parse_create_table( Keyword::COMMENT => { // Rewind the COMMENT keyword parser.prev_token(); - builder = builder.comment(parser.parse_optional_inline_comment()?); + if let Some(comment_def) = parser.parse_optional_inline_comment()? { + plain_options.push(SqlOption::Comment(comment_def)) + } } Keyword::AS => { let query = parser.parse_query()?; @@ -589,6 +593,13 @@ pub fn parse_create_table( } } } + let table_options = if !plain_options.is_empty() { + crate::ast::CreateTableOptions::Plain(plain_options) + } else { + crate::ast::CreateTableOptions::None + }; + + builder = builder.table_options(table_options); if iceberg && builder.base_location.is_none() { return Err(ParserError::ParserError( diff --git a/src/keywords.rs b/src/keywords.rs index bf8a1915..ddb78665 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -116,9 +116,11 @@ define_keywords!( AUTHENTICATION, AUTHORIZATION, AUTO, + AUTOEXTEND_SIZE, AUTOINCREMENT, AUTO_INCREMENT, AVG, + AVG_ROW_LENGTH, AVRO, BACKWARD, BASE64, @@ -180,6 +182,7 @@ define_keywords!( CHARSET, CHAR_LENGTH, CHECK, + CHECKSUM, CIRCLE, CLEAR, CLOB, @@ -269,6 +272,7 @@ define_keywords!( DEFINED, DEFINER, DELAYED, + DELAY_KEY_WRITE, DELETE, DELIMITED, DELIMITER, @@ -313,6 +317,7 @@ define_keywords!( END_PARTITION, ENFORCED, ENGINE, + ENGINE_ATTRIBUTE, ENUM, ENUM16, ENUM8, @@ -444,6 +449,7 @@ define_keywords!( INPUTFORMAT, INSENSITIVE, INSERT, + INSERT_METHOD, INSTALL, INSTANT, INSTEAD, @@ -480,6 +486,7 @@ define_keywords!( JULIAN, KEY, KEYS, + KEY_BLOCK_SIZE, KILL, LAG, LANGUAGE, @@ -533,6 +540,7 @@ define_keywords!( MAX, MAXVALUE, MAX_DATA_EXTENSION_TIME_IN_DAYS, + MAX_ROWS, MEASURES, MEDIUMBLOB, MEDIUMINT, @@ -554,6 +562,7 @@ define_keywords!( MINUTE, MINUTES, MINVALUE, + MIN_ROWS, MOD, MODE, MODIFIES, @@ -651,6 +660,7 @@ define_keywords!( OWNERSHIP, PACKAGE, PACKAGES, + PACK_KEYS, PARALLEL, PARAMETER, PARQUET, @@ -773,6 +783,7 @@ define_keywords!( ROW, ROWID, ROWS, + ROW_FORMAT, ROW_NUMBER, RULE, RUN, @@ -787,6 +798,7 @@ define_keywords!( SEARCH, SECOND, SECONDARY, + SECONDARY_ENGINE_ATTRIBUTE, SECONDS, SECRET, SECURITY, @@ -838,12 +850,16 @@ define_keywords!( STATEMENT, STATIC, STATISTICS, + STATS_AUTO_RECALC, + STATS_PERSISTENT, + STATS_SAMPLE_PAGES, STATUS, STDDEV_POP, STDDEV_SAMP, STDIN, STDOUT, STEP, + STORAGE, STORAGE_INTEGRATION, STORAGE_SERIALIZATION_POLICY, STORED, @@ -870,6 +886,7 @@ define_keywords!( TABLE, TABLES, TABLESAMPLE, + TABLESPACE, TAG, TARGET, TASK, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cbd464c3..a347f3d4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5524,12 +5524,17 @@ impl<'a> Parser<'a> { }; let location = hive_formats.location.clone(); let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; + let table_options = if !table_properties.is_empty() { + CreateTableOptions::TableProperties(table_properties) + } else { + CreateTableOptions::None + }; Ok(CreateTableBuilder::new(table_name) .columns(columns) .constraints(constraints) .hive_distribution(hive_distribution) .hive_formats(Some(hive_formats)) - .table_properties(table_properties) + .table_options(table_options) .or_replace(or_replace) .if_not_exists(if_not_exists) .external(true) @@ -7041,17 +7046,16 @@ impl<'a> Parser<'a> { // parse optional column list (schema) let (columns, constraints) = self.parse_columns()?; - let mut comment = if dialect_of!(self is HiveDialect) - && self.parse_keyword(Keyword::COMMENT) - { - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(str) => Some(CommentDef::AfterColumnDefsWithoutEq(str)), - _ => self.expected("comment", next_token)?, - } - } else { - None - }; + let comment_after_column_def = + if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { + let next_token = self.next_token(); + match next_token.token { + Token::SingleQuotedString(str) => Some(CommentDef::WithoutEq(str)), + _ => self.expected("comment", next_token)?, + } + } else { + None + }; // SQLite supports `WITHOUT ROWID` at the end of `CREATE TABLE` let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]); @@ -7059,39 +7063,8 @@ impl<'a> Parser<'a> { let hive_distribution = self.parse_hive_distribution()?; let clustered_by = self.parse_optional_clustered_by()?; let hive_formats = self.parse_hive_formats()?; - // PostgreSQL supports `WITH ( options )`, before `AS` - let with_options = self.parse_options(Keyword::WITH)?; - let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; - let engine = if self.parse_keyword(Keyword::ENGINE) { - self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => { - let name = w.value; - let parameters = if self.peek_token() == Token::LParen { - Some(self.parse_parenthesized_identifiers()?) - } else { - None - }; - Some(TableEngine { name, parameters }) - } - _ => self.expected("identifier", next_token)?, - } - } else { - None - }; - - let auto_increment_offset = if self.parse_keyword(Keyword::AUTO_INCREMENT) { - let _ = self.consume_token(&Token::Eq); - let next_token = self.next_token(); - match next_token.token { - Token::Number(s, _) => Some(Self::parse::(s, next_token.span.start)?), - _ => self.expected("literal int", next_token)?, - } - } else { - None - }; + let create_table_config = self.parse_optional_create_table_config()?; // ClickHouse supports `PRIMARY KEY`, before `ORDER BY` // https://clickhouse.com/docs/en/sql-reference/statements/create/table#primary-key @@ -7119,30 +7092,6 @@ impl<'a> Parser<'a> { None }; - let create_table_config = self.parse_optional_create_table_config()?; - - let default_charset = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARSET]) { - self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => Some(w.value), - _ => self.expected("identifier", next_token)?, - } - } else { - None - }; - - let collation = if self.parse_keywords(&[Keyword::COLLATE]) { - self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => Some(w.value), - _ => self.expected("identifier", next_token)?, - } - } else { - None - }; - let on_commit = if self.parse_keywords(&[Keyword::ON, Keyword::COMMIT]) { Some(self.parse_create_table_on_commit()?) } else { @@ -7151,13 +7100,6 @@ impl<'a> Parser<'a> { let strict = self.parse_keyword(Keyword::STRICT); - // Excludes Hive dialect here since it has been handled after table column definitions. - if !dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { - // rewind the COMMENT keyword - self.prev_token(); - comment = self.parse_optional_inline_comment()? - }; - // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { Some(self.parse_query()?) @@ -7174,8 +7116,6 @@ impl<'a> Parser<'a> { .temporary(temporary) .columns(columns) .constraints(constraints) - .with_options(with_options) - .table_properties(table_properties) .or_replace(or_replace) .if_not_exists(if_not_exists) .transient(transient) @@ -7186,19 +7126,15 @@ impl<'a> Parser<'a> { .without_rowid(without_rowid) .like(like) .clone_clause(clone) - .engine(engine) - .comment(comment) - .auto_increment_offset(auto_increment_offset) + .comment_after_column_def(comment_after_column_def) .order_by(order_by) - .default_charset(default_charset) - .collation(collation) .on_commit(on_commit) .on_cluster(on_cluster) .clustered_by(clustered_by) .partition_by(create_table_config.partition_by) .cluster_by(create_table_config.cluster_by) - .options(create_table_config.options) .inherits(create_table_config.inherits) + .table_options(create_table_config.table_options) .primary_key(primary_key) .strict(strict) .build()) @@ -7222,17 +7158,29 @@ impl<'a> Parser<'a> { /// Parse configuration like inheritance, partitioning, clustering information during the table creation. /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_2) - /// [PostgreSQL Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) - /// [PostgreSQL Inheritance](https://www.postgresql.org/docs/current/ddl-inherit.html) + /// [PostgreSQL](https://www.postgresql.org/docs/current/ddl-partitioning.html) + /// [MySql](https://dev.mysql.com/doc/refman/8.4/en/create-table.html) fn parse_optional_create_table_config( &mut self, ) -> Result { + let mut table_options = CreateTableOptions::None; + let inherits = if self.parse_keyword(Keyword::INHERITS) { Some(self.parse_parenthesized_qualified_column_list(IsOptional::Mandatory, false)?) } else { None }; + // PostgreSQL supports `WITH ( options )`, before `AS` + let with_options = self.parse_options(Keyword::WITH)?; + if !with_options.is_empty() { + table_options = CreateTableOptions::With(with_options) + } + + let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; + if !table_properties.is_empty() { + table_options = CreateTableOptions::TableProperties(table_properties); + } let partition_by = if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | GenericDialect) && self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { @@ -7242,7 +7190,6 @@ impl<'a> Parser<'a> { }; let mut cluster_by = None; - let mut options = None; if dialect_of!(self is BigQueryDialect | GenericDialect) { if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { cluster_by = Some(WrappedCollection::NoWrapping( @@ -7252,19 +7199,230 @@ impl<'a> Parser<'a> { if let Token::Word(word) = self.peek_token().token { if word.keyword == Keyword::OPTIONS { - options = Some(self.parse_options(Keyword::OPTIONS)?); + table_options = + CreateTableOptions::Options(self.parse_options(Keyword::OPTIONS)?) } }; } + if !dialect_of!(self is HiveDialect) && table_options == CreateTableOptions::None { + let plain_options = self.parse_plain_options()?; + if !plain_options.is_empty() { + table_options = CreateTableOptions::Plain(plain_options) + } + }; + Ok(CreateTableConfiguration { partition_by, cluster_by, - options, inherits, + table_options, }) } + fn parse_plain_option(&mut self) -> Result, ParserError> { + // Single parameter option + // + if self.parse_keywords(&[Keyword::START, Keyword::TRANSACTION]) { + return Ok(Some(SqlOption::Ident(Ident::new("START TRANSACTION")))); + } + + // Custom option + // + if self.parse_keywords(&[Keyword::COMMENT]) { + let has_eq = self.consume_token(&Token::Eq); + let value = self.next_token(); + + let comment = match (has_eq, value.token) { + (true, Token::SingleQuotedString(s)) => { + Ok(Some(SqlOption::Comment(CommentDef::WithEq(s)))) + } + (false, Token::SingleQuotedString(s)) => { + Ok(Some(SqlOption::Comment(CommentDef::WithoutEq(s)))) + } + (_, token) => { + self.expected("Token::SingleQuotedString", TokenWithSpan::wrap(token)) + } + }; + return comment; + } + + // + // + if self.parse_keywords(&[Keyword::ENGINE]) { + let _ = self.consume_token(&Token::Eq); + let value = self.next_token(); + + let engine = match value.token { + Token::Word(w) => { + let parameters = if self.peek_token() == Token::LParen { + self.parse_parenthesized_identifiers()? + } else { + vec![] + }; + + Ok(Some(SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new(w.value)), + values: parameters, + }, + ))) + } + _ => { + return self.expected("Token::Word", value)?; + } + }; + + return engine; + } + + // + if self.parse_keywords(&[Keyword::TABLESPACE]) { + let _ = self.consume_token(&Token::Eq); + let value = self.next_token(); + + let tablespace = match value.token { + Token::Word(Word { value: name, .. }) | Token::SingleQuotedString(name) => { + let storage = match self.parse_keyword(Keyword::STORAGE) { + true => { + let _ = self.consume_token(&Token::Eq); + let storage_token = self.next_token(); + match &storage_token.token { + Token::Word(w) => match w.value.to_uppercase().as_str() { + "DISK" => Some(StorageType::Disk), + "MEMORY" => Some(StorageType::Memory), + _ => self + .expected("Storage type (DISK or MEMORY)", storage_token)?, + }, + _ => self.expected("Token::Word", storage_token)?, + } + } + false => None, + }; + + Ok(Some(SqlOption::TableSpace(TablespaceOption { + name, + storage, + }))) + } + _ => { + return self.expected("Token::Word", value)?; + } + }; + + return tablespace; + } + + // + if self.parse_keyword(Keyword::UNION) { + let _ = self.consume_token(&Token::Eq); + let value = self.next_token(); + + match value.token { + Token::LParen => { + let tables: Vec = + self.parse_comma_separated0(Parser::parse_identifier, Token::RParen)?; + self.expect_token(&Token::RParen)?; + + return Ok(Some(SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("UNION"), + name: None, + values: tables, + }, + ))); + } + _ => { + return self.expected("Token::LParen", value)?; + } + } + } + + // Key/Value parameter option + let key = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARSET]) { + Ident::new("DEFAULT CHARSET") + } else if self.parse_keyword(Keyword::CHARSET) { + Ident::new("CHARSET") + } else if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARACTER, Keyword::SET]) { + Ident::new("DEFAULT CHARACTER SET") + } else if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) { + Ident::new("CHARACTER SET") + } else if self.parse_keywords(&[Keyword::DEFAULT, Keyword::COLLATE]) { + Ident::new("DEFAULT COLLATE") + } else if self.parse_keyword(Keyword::COLLATE) { + Ident::new("COLLATE") + } else if self.parse_keywords(&[Keyword::DATA, Keyword::DIRECTORY]) { + Ident::new("DATA DIRECTORY") + } else if self.parse_keywords(&[Keyword::INDEX, Keyword::DIRECTORY]) { + Ident::new("INDEX DIRECTORY") + } else if self.parse_keyword(Keyword::KEY_BLOCK_SIZE) { + Ident::new("KEY_BLOCK_SIZE") + } else if self.parse_keyword(Keyword::ROW_FORMAT) { + Ident::new("ROW_FORMAT") + } else if self.parse_keyword(Keyword::PACK_KEYS) { + Ident::new("PACK_KEYS") + } else if self.parse_keyword(Keyword::STATS_AUTO_RECALC) { + Ident::new("STATS_AUTO_RECALC") + } else if self.parse_keyword(Keyword::STATS_PERSISTENT) { + Ident::new("STATS_PERSISTENT") + } else if self.parse_keyword(Keyword::STATS_SAMPLE_PAGES) { + Ident::new("STATS_SAMPLE_PAGES") + } else if self.parse_keyword(Keyword::DELAY_KEY_WRITE) { + Ident::new("DELAY_KEY_WRITE") + } else if self.parse_keyword(Keyword::COMPRESSION) { + Ident::new("COMPRESSION") + } else if self.parse_keyword(Keyword::ENCRYPTION) { + Ident::new("ENCRYPTION") + } else if self.parse_keyword(Keyword::MAX_ROWS) { + Ident::new("MAX_ROWS") + } else if self.parse_keyword(Keyword::MIN_ROWS) { + Ident::new("MIN_ROWS") + } else if self.parse_keyword(Keyword::AUTOEXTEND_SIZE) { + Ident::new("AUTOEXTEND_SIZE") + } else if self.parse_keyword(Keyword::AVG_ROW_LENGTH) { + Ident::new("AVG_ROW_LENGTH") + } else if self.parse_keyword(Keyword::CHECKSUM) { + Ident::new("CHECKSUM") + } else if self.parse_keyword(Keyword::CONNECTION) { + Ident::new("CONNECTION") + } else if self.parse_keyword(Keyword::ENGINE_ATTRIBUTE) { + Ident::new("ENGINE_ATTRIBUTE") + } else if self.parse_keyword(Keyword::PASSWORD) { + Ident::new("PASSWORD") + } else if self.parse_keyword(Keyword::SECONDARY_ENGINE_ATTRIBUTE) { + Ident::new("SECONDARY_ENGINE_ATTRIBUTE") + } else if self.parse_keyword(Keyword::INSERT_METHOD) { + Ident::new("INSERT_METHOD") + } else if self.parse_keyword(Keyword::AUTO_INCREMENT) { + Ident::new("AUTO_INCREMENT") + } else { + return Ok(None); + }; + + let _ = self.consume_token(&Token::Eq); + + let value = match self + .maybe_parse(|parser| parser.parse_value())? + .map(Expr::Value) + { + Some(expr) => expr, + None => Expr::Identifier(self.parse_identifier()?), + }; + + Ok(Some(SqlOption::KeyValue { key, value })) + } + + pub fn parse_plain_options(&mut self) -> Result, ParserError> { + let mut options = Vec::new(); + + while let Some(option) = self.parse_plain_option()? { + options.push(option); + } + + Ok(options) + } + pub fn parse_optional_inline_comment(&mut self) -> Result, ParserError> { let comment = if self.parse_keyword(Keyword::COMMENT) { let has_eq = self.consume_token(&Token::Eq); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 416d2e43..8f54f3c9 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -484,7 +484,7 @@ fn parse_create_table_with_options() { columns, partition_by, cluster_by, - options, + table_options, .. }) => { assert_eq!( @@ -539,7 +539,7 @@ fn parse_create_table_with_options() { Ident::new("userid"), Ident::new("age"), ])), - Some(vec![ + CreateTableOptions::Options(vec![ SqlOption::KeyValue { key: Ident::new("partition_expiration_days"), value: Expr::Value( @@ -561,7 +561,7 @@ fn parse_create_table_with_options() { }, ]) ), - (partition_by, cluster_by, options) + (partition_by, cluster_by, table_options) ) } _ => unreachable!(), diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index c56f9886..d0218b6c 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -219,10 +219,10 @@ fn parse_delimited_identifiers() { #[test] fn parse_create_table() { - clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#); - clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x""#); + clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY ("x")"#); + clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY "x""#); clickhouse().verified_stmt( - r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x" AS SELECT * FROM "t" WHERE true"#, + r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY "x" AS SELECT * FROM "t" WHERE true"#, ); } @@ -589,7 +589,7 @@ fn parse_clickhouse_data_types() { #[test] fn parse_create_table_with_nullable() { - let sql = r#"CREATE TABLE table (k UInt8, `a` Nullable(String), `b` Nullable(DateTime64(9, 'UTC')), c Nullable(DateTime64(9)), d Date32 NULL) ENGINE=MergeTree ORDER BY (`k`)"#; + let sql = r#"CREATE TABLE table (k UInt8, `a` Nullable(String), `b` Nullable(DateTime64(9, 'UTC')), c Nullable(DateTime64(9)), d Date32 NULL) ENGINE = MergeTree ORDER BY (`k`)"#; // ClickHouse has a case-sensitive definition of data type, but canonical representation is not let canonical_sql = sql.replace("String", "STRING"); @@ -714,14 +714,14 @@ fn parse_create_table_with_nested_data_types() { fn parse_create_table_with_primary_key() { match clickhouse_and_generic().verified_stmt(concat!( r#"CREATE TABLE db.table (`i` INT, `k` INT)"#, - " ENGINE=SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')", + " ENGINE = SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')", " PRIMARY KEY tuple(i)", " ORDER BY tuple(i)", )) { Statement::CreateTable(CreateTable { name, columns, - engine, + table_options, primary_key, order_by, .. @@ -742,16 +742,23 @@ fn parse_create_table_with_primary_key() { ], columns ); - assert_eq!( - engine, - Some(TableEngine { - name: "SharedMergeTree".to_string(), - parameters: Some(vec![ + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new("SharedMergeTree")), + values: vec![ Ident::with_quote('\'', "/clickhouse/tables/{uuid}/{shard}"), Ident::with_quote('\'', "{replica}"), - ]), - }) - ); + ] + } + ))); + fn assert_function(actual: &Function, name: &str, arg: &str) -> bool { assert_eq!(actual.name, ObjectName::from(vec![Ident::new(name)])); assert_eq!( @@ -798,7 +805,7 @@ fn parse_create_table_with_variant_default_expressions() { " b DATETIME EPHEMERAL now(),", " c DATETIME EPHEMERAL,", " d STRING ALIAS toString(c)", - ") ENGINE=MergeTree" + ") ENGINE = MergeTree" ); match clickhouse_and_generic().verified_stmt(sql) { Statement::CreateTable(CreateTable { columns, .. }) => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1ddf3f92..7a8b8bda 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3657,7 +3657,7 @@ fn parse_create_table() { name, columns, constraints, - with_options, + table_options, if_not_exists: false, external: false, file_format: None, @@ -3795,7 +3795,7 @@ fn parse_create_table() { }, ] ); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); } _ => unreachable!(), } @@ -3840,7 +3840,7 @@ fn parse_create_table_with_constraint_characteristics() { name, columns, constraints, - with_options, + table_options, if_not_exists: false, external: false, file_format: None, @@ -3934,7 +3934,7 @@ fn parse_create_table_with_constraint_characteristics() { }, ] ); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); } _ => unreachable!(), } @@ -4421,7 +4421,11 @@ fn parse_create_table_with_options() { let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; match generic.verified_stmt(sql) { - Statement::CreateTable(CreateTable { with_options, .. }) => { + Statement::CreateTable(CreateTable { table_options, .. }) => { + let with_options = match table_options { + CreateTableOptions::With(options) => options, + _ => unreachable!(), + }; assert_eq!( vec![ SqlOption::KeyValue { @@ -4482,7 +4486,7 @@ fn parse_create_external_table() { name, columns, constraints, - with_options, + table_options, if_not_exists, external, file_format, @@ -4525,7 +4529,7 @@ fn parse_create_external_table() { assert_eq!(FileFormat::TEXTFILE, file_format.unwrap()); assert_eq!("/tmp/example.csv", location.unwrap()); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); assert!(!if_not_exists); } _ => unreachable!(), @@ -4550,7 +4554,7 @@ fn parse_create_or_replace_external_table() { name, columns, constraints, - with_options, + table_options, if_not_exists, external, file_format, @@ -4579,7 +4583,7 @@ fn parse_create_or_replace_external_table() { assert_eq!(FileFormat::TEXTFILE, file_format.unwrap()); assert_eq!("/tmp/example.csv", location.unwrap()); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); assert!(!if_not_exists); assert!(or_replace); } @@ -11420,7 +11424,9 @@ fn test_parse_inline_comment() { // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::CreateTable(CreateTable { - columns, comment, .. + columns, + table_options, + .. }) => { assert_eq!( columns, @@ -11434,8 +11440,10 @@ fn test_parse_inline_comment() { }] ); assert_eq!( - comment.unwrap(), - CommentDef::WithEq("comment with equal".to_string()) + table_options, + CreateTableOptions::Plain(vec![SqlOption::Comment(CommentDef::WithEq( + "comment with equal".to_string() + ))]) ); } _ => unreachable!(), @@ -12460,21 +12468,6 @@ fn parse_select_wildcard_with_except() { ); } -#[test] -fn parse_auto_increment_too_large() { - let dialect = GenericDialect {}; - let u64_max = u64::MAX; - let sql = - format!("CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) AUTO_INCREMENT=1{u64_max}"); - - let res = Parser::new(&dialect) - .try_with_sql(&sql) - .expect("tokenize to work") - .parse_statements(); - - assert!(res.is_err(), "{res:?}"); -} - #[test] fn test_group_by_nothing() { let Select { group_by, .. } = all_dialects_where(|d| d.supports_group_by_expr()) diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 32058324..8e498365 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -735,19 +735,13 @@ fn test_duckdb_union_datatype() { storage: Default::default(), location: Default::default() }), - table_properties: Default::default(), - with_options: Default::default(), file_format: Default::default(), location: Default::default(), query: Default::default(), without_rowid: Default::default(), like: Default::default(), clone: Default::default(), - engine: Default::default(), comment: Default::default(), - auto_increment_offset: Default::default(), - default_charset: Default::default(), - collation: Default::default(), on_commit: Default::default(), on_cluster: Default::default(), primary_key: Default::default(), @@ -755,7 +749,6 @@ fn test_duckdb_union_datatype() { partition_by: Default::default(), cluster_by: Default::default(), clustered_by: Default::default(), - options: Default::default(), inherits: Default::default(), strict: Default::default(), copy_grants: Default::default(), @@ -772,6 +765,7 @@ fn test_duckdb_union_datatype() { catalog: Default::default(), catalog_sync: Default::default(), storage_serialization_policy: Default::default(), + table_options: CreateTableOptions::None }), stmt ); diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 9b043094..14dcbffd 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -133,9 +133,7 @@ fn create_table_with_comment() { Statement::CreateTable(CreateTable { comment, .. }) => { assert_eq!( comment, - Some(CommentDef::AfterColumnDefsWithoutEq( - "table comment".to_string() - )) + Some(CommentDef::WithoutEq("table comment".to_string())) ) } _ => unreachable!(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 88e7a1f1..8cc5758f 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1725,7 +1725,6 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - options: vec![], }, ColumnDef { @@ -1735,7 +1734,6 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - options: vec![], }, ], @@ -1747,19 +1745,13 @@ fn parse_create_table_with_valid_options() { storage: None, location: None, },), - table_properties: vec![], - with_options, file_format: None, location: None, query: None, without_rowid: false, like: None, clone: None, - engine: None, comment: None, - auto_increment_offset: None, - default_charset: None, - collation: None, on_commit: None, on_cluster: None, primary_key: None, @@ -1767,7 +1759,6 @@ fn parse_create_table_with_valid_options() { partition_by: None, cluster_by: None, clustered_by: None, - options: None, inherits: None, strict: false, iceberg: false, @@ -1785,6 +1776,7 @@ fn parse_create_table_with_valid_options() { catalog: None, catalog_sync: None, storage_serialization_policy: None, + table_options: CreateTableOptions::With(with_options) }) ); } @@ -1918,19 +1910,13 @@ fn parse_create_table_with_identity_column() { storage: None, location: None, },), - table_properties: vec![], - with_options: vec![], file_format: None, location: None, query: None, without_rowid: false, like: None, clone: None, - engine: None, comment: None, - auto_increment_offset: None, - default_charset: None, - collation: None, on_commit: None, on_cluster: None, primary_key: None, @@ -1938,7 +1924,6 @@ fn parse_create_table_with_identity_column() { partition_by: None, cluster_by: None, clustered_by: None, - options: None, inherits: None, strict: false, copy_grants: false, @@ -1955,6 +1940,7 @@ fn parse_create_table_with_identity_column() { catalog: None, catalog_sync: None, storage_serialization_policy: None, + table_options: CreateTableOptions::None }), ); } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 990107b2..4bb1063d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -848,9 +848,23 @@ fn parse_create_table_comment() { for sql in [without_equal, with_equal] { match mysql().verified_stmt(sql) { - Statement::CreateTable(CreateTable { name, comment, .. }) => { + Statement::CreateTable(CreateTable { + name, + table_options, + .. + }) => { assert_eq!(name.to_string(), "foo"); - assert_eq!(comment.expect("Should exist").to_string(), "baz"); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + let comment = match plain_options.first().unwrap() { + SqlOption::Comment(CommentDef::WithEq(c)) + | SqlOption::Comment(CommentDef::WithoutEq(c)) => c, + _ => unreachable!(), + }; + assert_eq!(comment, "baz"); } _ => unreachable!(), } @@ -859,29 +873,226 @@ fn parse_create_table_comment() { #[test] fn parse_create_table_auto_increment_offset() { - let canonical = - "CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT 123"; - let with_equal = - "CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123"; + let sql = + "CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) ENGINE = InnoDB AUTO_INCREMENT = 123"; - for sql in [canonical, with_equal] { - match mysql().one_statement_parses_to(sql, canonical) { + match mysql().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + table_options, + .. + }) => { + assert_eq!(name.to_string(), "foo"); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("AUTO_INCREMENT"), + value: Expr::Value(test_utils::number("123").with_empty_span()) + })); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_table_multiple_options_order_independent() { + let sql1 = "CREATE TABLE mytable (id INT) ENGINE=InnoDB ROW_FORMAT=DYNAMIC KEY_BLOCK_SIZE=8 COMMENT='abc'"; + let sql2 = "CREATE TABLE mytable (id INT) KEY_BLOCK_SIZE=8 COMMENT='abc' ENGINE=InnoDB ROW_FORMAT=DYNAMIC"; + let sql3 = "CREATE TABLE mytable (id INT) ROW_FORMAT=DYNAMIC KEY_BLOCK_SIZE=8 COMMENT='abc' ENGINE=InnoDB"; + + for sql in [sql1, sql2, sql3] { + match mysql().parse_sql_statements(sql).unwrap().pop().unwrap() { Statement::CreateTable(CreateTable { name, - auto_increment_offset, + table_options, .. }) => { - assert_eq!(name.to_string(), "foo"); - assert_eq!( - auto_increment_offset.expect("Should exist").to_string(), - "123" - ); + assert_eq!(name.to_string(), "mytable"); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new("InnoDB")), + values: vec![] + } + ))); + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("KEY_BLOCK_SIZE"), + value: Expr::Value(test_utils::number("8").with_empty_span()) + })); + + assert!(plain_options + .contains(&SqlOption::Comment(CommentDef::WithEq("abc".to_owned())))); + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("ROW_FORMAT"), + value: Expr::Identifier(Ident::new("DYNAMIC".to_owned())) + })); } _ => unreachable!(), } } } +#[test] +fn parse_create_table_with_all_table_options() { + let sql = + "CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) ENGINE = InnoDB AUTO_INCREMENT = 123 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci INSERT_METHOD = FIRST KEY_BLOCK_SIZE = 8 ROW_FORMAT = DYNAMIC DATA DIRECTORY = '/var/lib/mysql/data' INDEX DIRECTORY = '/var/lib/mysql/index' PACK_KEYS = 1 STATS_AUTO_RECALC = 1 STATS_PERSISTENT = 0 STATS_SAMPLE_PAGES = 128 DELAY_KEY_WRITE = 1 COMPRESSION = 'ZLIB' ENCRYPTION = 'Y' MAX_ROWS = 10000 MIN_ROWS = 10 AUTOEXTEND_SIZE = 64 AVG_ROW_LENGTH = 128 CHECKSUM = 1 CONNECTION = 'mysql://localhost' ENGINE_ATTRIBUTE = 'primary' PASSWORD = 'secure_password' SECONDARY_ENGINE_ATTRIBUTE = 'secondary_attr' START TRANSACTION TABLESPACE my_tablespace STORAGE DISK UNION = (table1, table2, table3)"; + + match mysql().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + table_options, + .. + }) => { + assert_eq!(name, vec![Ident::new("foo".to_owned())].into()); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new("InnoDB")), + values: vec![] + } + ))); + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("COLLATE"), + value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("DEFAULT CHARSET"), + value: Expr::Identifier(Ident::new("utf8mb4".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("AUTO_INCREMENT"), + value: Expr::value(test_utils::number("123")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("KEY_BLOCK_SIZE"), + value: Expr::value(test_utils::number("8")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("ROW_FORMAT"), + value: Expr::Identifier(Ident::new("DYNAMIC".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("PACK_KEYS"), + value: Expr::value(test_utils::number("1")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("STATS_AUTO_RECALC"), + value: Expr::value(test_utils::number("1")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("STATS_PERSISTENT"), + value: Expr::value(test_utils::number("0")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("STATS_SAMPLE_PAGES"), + value: Expr::value(test_utils::number("128")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("STATS_SAMPLE_PAGES"), + value: Expr::value(test_utils::number("128")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("INSERT_METHOD"), + value: Expr::Identifier(Ident::new("FIRST".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("COMPRESSION"), + value: Expr::value(Value::SingleQuotedString("ZLIB".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("ENCRYPTION"), + value: Expr::value(Value::SingleQuotedString("Y".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("MAX_ROWS"), + value: Expr::value(test_utils::number("10000")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("MIN_ROWS"), + value: Expr::value(test_utils::number("10")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("AUTOEXTEND_SIZE"), + value: Expr::value(test_utils::number("64")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("AVG_ROW_LENGTH"), + value: Expr::value(test_utils::number("128")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("CHECKSUM"), + value: Expr::value(test_utils::number("1")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("CONNECTION"), + value: Expr::value(Value::SingleQuotedString("mysql://localhost".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("ENGINE_ATTRIBUTE"), + value: Expr::value(Value::SingleQuotedString("primary".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("PASSWORD"), + value: Expr::value(Value::SingleQuotedString("secure_password".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("SECONDARY_ENGINE_ATTRIBUTE"), + value: Expr::value(Value::SingleQuotedString("secondary_attr".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::Ident(Ident::new( + "START TRANSACTION".to_owned() + )))); + assert!( + plain_options.contains(&SqlOption::TableSpace(TablespaceOption { + name: "my_tablespace".to_string(), + storage: Some(StorageType::Disk), + })) + ); + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("UNION"), + name: None, + values: vec![ + Ident::new("table1".to_string()), + Ident::new("table2".to_string()), + Ident::new("table3".to_string()) + ] + } + ))); + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("DATA DIRECTORY"), + value: Expr::value(Value::SingleQuotedString("/var/lib/mysql/data".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("INDEX DIRECTORY"), + value: Expr::value(Value::SingleQuotedString("/var/lib/mysql/index".to_owned())) + })); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_set_enum() { let sql = "CREATE TABLE foo (bar SET('a', 'b'), baz ENUM('a', 'b'))"; @@ -916,13 +1127,12 @@ fn parse_create_table_set_enum() { #[test] fn parse_create_table_engine_default_charset() { - let sql = "CREATE TABLE foo (id INT(11)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"; + let sql = "CREATE TABLE foo (id INT(11)) ENGINE = InnoDB DEFAULT CHARSET = utf8mb3"; match mysql().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, columns, - engine, - default_charset, + table_options, .. }) => { assert_eq!(name.to_string(), "foo"); @@ -934,14 +1144,24 @@ fn parse_create_table_engine_default_charset() { },], columns ); - assert_eq!( - engine, - Some(TableEngine { - name: "InnoDB".to_string(), - parameters: None - }) - ); - assert_eq!(default_charset, Some("utf8mb3".to_string())); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("DEFAULT CHARSET"), + value: Expr::Identifier(Ident::new("utf8mb3".to_owned())) + })); + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new("InnoDB")), + values: vec![] + } + ))); } _ => unreachable!(), } @@ -949,12 +1169,12 @@ fn parse_create_table_engine_default_charset() { #[test] fn parse_create_table_collate() { - let sql = "CREATE TABLE foo (id INT(11)) COLLATE=utf8mb4_0900_ai_ci"; + let sql = "CREATE TABLE foo (id INT(11)) COLLATE = utf8mb4_0900_ai_ci"; match mysql().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, columns, - collation, + table_options, .. }) => { assert_eq!(name.to_string(), "foo"); @@ -966,7 +1186,16 @@ fn parse_create_table_collate() { },], columns ); - assert_eq!(collation, Some("utf8mb4_0900_ai_ci".to_string())); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("COLLATE"), + value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) + })); } _ => unreachable!(), } @@ -974,16 +1203,26 @@ fn parse_create_table_collate() { #[test] fn parse_create_table_both_options_and_as_query() { - let sql = "CREATE TABLE foo (id INT(11)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb4_0900_ai_ci AS SELECT 1"; + let sql = "CREATE TABLE foo (id INT(11)) ENGINE = InnoDB DEFAULT CHARSET = utf8mb3 COLLATE = utf8mb4_0900_ai_ci AS SELECT 1"; match mysql_and_generic().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, - collation, query, + table_options, .. }) => { assert_eq!(name.to_string(), "foo"); - assert_eq!(collation, Some("utf8mb4_0900_ai_ci".to_string())); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("COLLATE"), + value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) + })); + assert_eq!( query.unwrap().body.as_select().unwrap().projection, vec![SelectItem::UnnamedExpr(Expr::Value( @@ -994,7 +1233,8 @@ fn parse_create_table_both_options_and_as_query() { _ => unreachable!(), } - let sql = r"CREATE TABLE foo (id INT(11)) ENGINE=InnoDB AS SELECT 1 DEFAULT CHARSET=utf8mb3"; + let sql = + r"CREATE TABLE foo (id INT(11)) ENGINE = InnoDB AS SELECT 1 DEFAULT CHARSET = utf8mb3"; assert!(matches!( mysql_and_generic().parse_sql_statements(sql), Err(ParserError::ParserError(_)) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6c008c84..1fb7432a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -348,7 +348,7 @@ fn parse_create_table_with_defaults() { name, columns, constraints, - with_options, + table_options, if_not_exists: false, external: false, file_format: None, @@ -485,6 +485,11 @@ fn parse_create_table_with_defaults() { ] ); assert!(constraints.is_empty()); + + let with_options = match table_options { + CreateTableOptions::With(options) => options, + _ => unreachable!(), + }; assert_eq!( with_options, vec![ @@ -4668,7 +4673,6 @@ fn parse_create_table_with_alias() { name, columns, constraints, - with_options: _with_options, if_not_exists: false, external: false, file_format: None, @@ -5078,7 +5082,11 @@ fn parse_at_time_zone() { fn parse_create_table_with_options() { let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; match pg().verified_stmt(sql) { - Statement::CreateTable(CreateTable { with_options, .. }) => { + Statement::CreateTable(CreateTable { table_options, .. }) => { + let with_options = match table_options { + CreateTableOptions::With(options) => options, + _ => unreachable!(), + }; assert_eq!( vec![ SqlOption::KeyValue { @@ -5506,19 +5514,13 @@ fn parse_trigger_related_functions() { storage: None, location: None }), - table_properties: vec![], - with_options: vec![], file_format: None, location: None, query: None, without_rowid: false, like: None, clone: None, - engine: None, comment: None, - auto_increment_offset: None, - default_charset: None, - collation: None, on_commit: None, on_cluster: None, primary_key: None, @@ -5526,7 +5528,6 @@ fn parse_trigger_related_functions() { partition_by: None, cluster_by: None, clustered_by: None, - options: None, inherits: None, strict: false, copy_grants: false, @@ -5543,6 +5544,7 @@ fn parse_trigger_related_functions() { catalog: None, catalog_sync: None, storage_serialization_policy: None, + table_options: CreateTableOptions::None } ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index aa974115..52be3143 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -470,9 +470,22 @@ fn test_snowflake_create_table_cluster_by() { #[test] fn test_snowflake_create_table_comment() { match snowflake().verified_stmt("CREATE TABLE my_table (a INT) COMMENT = 'some comment'") { - Statement::CreateTable(CreateTable { name, comment, .. }) => { + Statement::CreateTable(CreateTable { + name, + table_options, + .. + }) => { assert_eq!("my_table", name.to_string()); - assert_eq!("some comment", comment.unwrap().to_string()); + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + let comment = match plain_options.first().unwrap() { + SqlOption::Comment(CommentDef::WithEq(c)) + | SqlOption::Comment(CommentDef::WithoutEq(c)) => c, + _ => unreachable!(), + }; + assert_eq!("some comment", comment); } _ => unreachable!(), } From a497358c3a3ef24cb346f5e8f071c3bd65fd0cdc Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Sat, 3 May 2025 10:59:13 -0400 Subject: [PATCH 055/130] Add `CREATE TRIGGER` support for SQL Server (#1810) --- src/ast/mod.rs | 48 +++++++++++++---- src/ast/trigger.rs | 2 + src/dialect/mssql.rs | 46 ++++++++++++++++ src/parser/mod.rs | 51 ++++++++++++------ tests/sqlparser_mssql.rs | 105 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_mysql.rs | 6 ++- tests/sqlparser_postgres.rs | 38 ++++++++----- 7 files changed, 255 insertions(+), 41 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d74d197e..c3009743 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2380,11 +2380,16 @@ impl fmt::Display for BeginEndStatements { end_token: AttachedToken(end_token), } = self; - write!(f, "{begin_token} ")?; + if begin_token.token != Token::EOF { + write!(f, "{begin_token} ")?; + } if !statements.is_empty() { format_statement_list(f, statements)?; } - write!(f, " {end_token}") + if end_token.token != Token::EOF { + write!(f, " {end_token}")?; + } + Ok(()) } } @@ -3729,7 +3734,12 @@ pub enum Statement { /// ``` /// /// Postgres: + /// SQL Server: CreateTrigger { + /// True if this is a `CREATE OR ALTER TRIGGER` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver16#arguments) + or_alter: bool, /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. /// /// Example: @@ -3790,7 +3800,9 @@ pub enum Statement { /// Triggering conditions condition: Option, /// Execute logic block - exec_body: TriggerExecBody, + exec_body: Option, + /// For SQL dialects with statement(s) for a body + statements: Option, /// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`, characteristics: Option, }, @@ -4587,6 +4599,7 @@ impl fmt::Display for Statement { } Statement::CreateFunction(create_function) => create_function.fmt(f), Statement::CreateTrigger { + or_alter, or_replace, is_constraint, name, @@ -4599,19 +4612,30 @@ impl fmt::Display for Statement { condition, include_each, exec_body, + statements, characteristics, } => { write!( f, - "CREATE {or_replace}{is_constraint}TRIGGER {name} {period}", + "CREATE {or_alter}{or_replace}{is_constraint}TRIGGER {name} ", + or_alter = if *or_alter { "OR ALTER " } else { "" }, or_replace = if *or_replace { "OR REPLACE " } else { "" }, is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, )?; - if !events.is_empty() { - write!(f, " {}", display_separated(events, " OR "))?; + if exec_body.is_some() { + write!(f, "{period}")?; + if !events.is_empty() { + write!(f, " {}", display_separated(events, " OR "))?; + } + write!(f, " ON {table_name}")?; + } else { + write!(f, "ON {table_name}")?; + write!(f, " {period}")?; + if !events.is_empty() { + write!(f, " {}", display_separated(events, ", "))?; + } } - write!(f, " ON {table_name}")?; if let Some(referenced_table_name) = referenced_table_name { write!(f, " FROM {referenced_table_name}")?; @@ -4627,13 +4651,19 @@ impl fmt::Display for Statement { if *include_each { write!(f, " FOR EACH {trigger_object}")?; - } else { + } else if exec_body.is_some() { write!(f, " FOR {trigger_object}")?; } if let Some(condition) = condition { write!(f, " WHEN {condition}")?; } - write!(f, " EXECUTE {exec_body}") + if let Some(exec_body) = exec_body { + write!(f, " EXECUTE {exec_body}")?; + } + if let Some(statements) = statements { + write!(f, " AS {statements}")?; + } + Ok(()) } Statement::DropTrigger { if_exists, diff --git a/src/ast/trigger.rs b/src/ast/trigger.rs index cf1c8c46..2c64e423 100644 --- a/src/ast/trigger.rs +++ b/src/ast/trigger.rs @@ -110,6 +110,7 @@ impl fmt::Display for TriggerEvent { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TriggerPeriod { + For, After, Before, InsteadOf, @@ -118,6 +119,7 @@ pub enum TriggerPeriod { impl fmt::Display for TriggerPeriod { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + TriggerPeriod::For => write!(f, "FOR"), TriggerPeriod::After => write!(f, "AFTER"), TriggerPeriod::Before => write!(f, "BEFORE"), TriggerPeriod::InsteadOf => write!(f, "INSTEAD OF"), diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 31e324f0..647e82a2 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -18,6 +18,7 @@ use crate::ast::helpers::attached_token::AttachedToken; use crate::ast::{ BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, IfStatement, Statement, + TriggerObject, }; use crate::dialect::Dialect; use crate::keywords::{self, Keyword}; @@ -125,6 +126,15 @@ impl Dialect for MsSqlDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.peek_keyword(Keyword::IF) { Some(self.parse_if_stmt(parser)) + } else if parser.parse_keywords(&[Keyword::CREATE, Keyword::TRIGGER]) { + Some(self.parse_create_trigger(parser, false)) + } else if parser.parse_keywords(&[ + Keyword::CREATE, + Keyword::OR, + Keyword::ALTER, + Keyword::TRIGGER, + ]) { + Some(self.parse_create_trigger(parser, true)) } else { None } @@ -215,6 +225,42 @@ impl MsSqlDialect { })) } + /// Parse `CREATE TRIGGER` for [MsSql] + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql + fn parse_create_trigger( + &self, + parser: &mut Parser, + or_alter: bool, + ) -> Result { + let name = parser.parse_object_name(false)?; + parser.expect_keyword_is(Keyword::ON)?; + let table_name = parser.parse_object_name(false)?; + let period = parser.parse_trigger_period()?; + let events = parser.parse_comma_separated(Parser::parse_trigger_event)?; + + parser.expect_keyword_is(Keyword::AS)?; + let statements = Some(parser.parse_conditional_statements(&[Keyword::END])?); + + Ok(Statement::CreateTrigger { + or_alter, + or_replace: false, + is_constraint: false, + name, + period, + events, + table_name, + referenced_table_name: None, + referencing: Vec::new(), + trigger_object: TriggerObject::Statement, + include_each: false, + condition: None, + exec_body: None, + statements, + characteristics: None, + }) + } + /// Parse a sequence of statements, optionally separated by semicolon. /// /// Stops parsing when reaching EOF or the given keyword. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a347f3d4..2011d31e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -745,19 +745,7 @@ impl<'a> Parser<'a> { } }; - let conditional_statements = if self.peek_keyword(Keyword::BEGIN) { - let begin_token = self.expect_keyword(Keyword::BEGIN)?; - let statements = self.parse_statement_list(terminal_keywords)?; - let end_token = self.expect_keyword(Keyword::END)?; - ConditionalStatements::BeginEnd(BeginEndStatements { - begin_token: AttachedToken(begin_token), - statements, - end_token: AttachedToken(end_token), - }) - } else { - let statements = self.parse_statement_list(terminal_keywords)?; - ConditionalStatements::Sequence { statements } - }; + let conditional_statements = self.parse_conditional_statements(terminal_keywords)?; Ok(ConditionalStatementBlock { start_token: AttachedToken(start_token), @@ -767,6 +755,30 @@ impl<'a> Parser<'a> { }) } + /// Parse a BEGIN/END block or a sequence of statements + /// This could be inside of a conditional (IF, CASE, WHILE etc.) or an object body defined optionally BEGIN/END and one or more statements. + pub(crate) fn parse_conditional_statements( + &mut self, + terminal_keywords: &[Keyword], + ) -> Result { + let conditional_statements = if self.peek_keyword(Keyword::BEGIN) { + let begin_token = self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(terminal_keywords)?; + let end_token = self.expect_keyword(Keyword::END)?; + + ConditionalStatements::BeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + }) + } else { + ConditionalStatements::Sequence { + statements: self.parse_statement_list(terminal_keywords)?, + } + }; + Ok(conditional_statements) + } + /// Parse a `RAISE` statement. /// /// See [Statement::Raise] @@ -4614,9 +4626,9 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::FUNCTION) { self.parse_create_function(or_alter, or_replace, temporary) } else if self.parse_keyword(Keyword::TRIGGER) { - self.parse_create_trigger(or_replace, false) + self.parse_create_trigger(or_alter, or_replace, false) } else if self.parse_keywords(&[Keyword::CONSTRAINT, Keyword::TRIGGER]) { - self.parse_create_trigger(or_replace, true) + self.parse_create_trigger(or_alter, or_replace, true) } else if self.parse_keyword(Keyword::MACRO) { self.parse_create_macro(or_replace, temporary) } else if self.parse_keyword(Keyword::SECRET) { @@ -5314,10 +5326,11 @@ impl<'a> Parser<'a> { pub fn parse_create_trigger( &mut self, + or_alter: bool, or_replace: bool, is_constraint: bool, ) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) { + if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); } @@ -5363,6 +5376,7 @@ impl<'a> Parser<'a> { let exec_body = self.parse_trigger_exec_body()?; Ok(Statement::CreateTrigger { + or_alter, or_replace, is_constraint, name, @@ -5374,7 +5388,8 @@ impl<'a> Parser<'a> { trigger_object, include_each, condition, - exec_body, + exec_body: Some(exec_body), + statements: None, characteristics, }) } @@ -5382,10 +5397,12 @@ impl<'a> Parser<'a> { pub fn parse_trigger_period(&mut self) -> Result { Ok( match self.expect_one_of_keywords(&[ + Keyword::FOR, Keyword::BEFORE, Keyword::AFTER, Keyword::INSTEAD, ])? { + Keyword::FOR => TriggerPeriod::For, Keyword::BEFORE => TriggerPeriod::Before, Keyword::AFTER => TriggerPeriod::After, Keyword::INSTEAD => self diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 8cc5758f..9ff55198 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -273,6 +273,16 @@ fn parse_create_function() { END\ "; let _ = ms().verified_stmt(create_or_alter_function); + + let create_function_with_return_expression = "\ + CREATE FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) \ + RETURNS INT \ + AS \ + BEGIN \ + RETURN CONVERT(INT, 1) + 2; \ + END\ + "; + let _ = ms().verified_stmt(create_function_with_return_expression); } #[test] @@ -2199,6 +2209,101 @@ fn parse_mssql_merge_with_output() { ms_and_generic().verified_stmt(stmt); } +#[test] +fn parse_create_trigger() { + let create_trigger = "\ + CREATE OR ALTER TRIGGER reminder1 \ + ON Sales.Customer \ + AFTER INSERT, UPDATE \ + AS RAISERROR('Notify Customer Relations', 16, 10);\ + "; + let create_stmt = ms().verified_stmt(create_trigger); + assert_eq!( + create_stmt, + Statement::CreateTrigger { + or_alter: true, + or_replace: false, + is_constraint: false, + name: ObjectName::from(vec![Ident::new("reminder1")]), + period: TriggerPeriod::After, + events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![]),], + table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Statement, + include_each: false, + condition: None, + exec_body: None, + statements: Some(ConditionalStatements::Sequence { + statements: vec![Statement::RaisError { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("Notify Customer Relations".to_string())) + .with_empty_span() + )), + severity: Box::new(Expr::Value( + (Value::Number("16".parse().unwrap(), false)).with_empty_span() + )), + state: Box::new(Expr::Value( + (Value::Number("10".parse().unwrap(), false)).with_empty_span() + )), + arguments: vec![], + options: vec![], + }], + }), + characteristics: None, + } + ); + + let multi_statement_as_trigger = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + DECLARE @var INT; \ + RAISERROR('Trigger fired', 10, 1);\ + "; + let _ = ms().verified_stmt(multi_statement_as_trigger); + + let multi_statement_trigger = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + DECLARE @var INT; \ + RAISERROR('Trigger fired', 10, 1); \ + END\ + "; + let _ = ms().verified_stmt(multi_statement_trigger); + + let create_trigger_with_return = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_return); + + let create_trigger_with_return = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_return); + + let create_trigger_with_conditional = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + IF 1 = 2 \ + BEGIN \ + RAISERROR('Trigger fired', 10, 1); \ + END; \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_conditional); +} + #[test] fn parse_drop_trigger() { let sql_drop_trigger = "DROP TRIGGER emp_stamp;"; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 4bb1063d..27c60b05 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3779,6 +3779,7 @@ fn parse_create_trigger() { assert_eq!( create_stmt, Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), @@ -3790,13 +3791,14 @@ fn parse_create_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("emp_stamp")]), args: None, } - }, + }), + statements: None, characteristics: None, } ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1fb7432a..008d0670 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5157,6 +5157,7 @@ fn test_escaped_string_literal() { fn parse_create_simple_before_insert_trigger() { let sql = "CREATE TRIGGER check_insert BEFORE INSERT ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_insert"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_insert")]), @@ -5168,13 +5169,14 @@ fn parse_create_simple_before_insert_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_insert")]), args: None, }, - }, + }), + statements: None, characteristics: None, }; @@ -5185,6 +5187,7 @@ fn parse_create_simple_before_insert_trigger() { fn parse_create_after_update_trigger_with_condition() { let sql = "CREATE TRIGGER check_update AFTER UPDATE ON accounts FOR EACH ROW WHEN (NEW.balance > 10000) EXECUTE FUNCTION check_account_update"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_update")]), @@ -5203,13 +5206,14 @@ fn parse_create_after_update_trigger_with_condition() { op: BinaryOperator::Gt, right: Box::new(Expr::value(number("10000"))), }))), - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_update")]), args: None, }, - }, + }), + statements: None, characteristics: None, }; @@ -5220,6 +5224,7 @@ fn parse_create_after_update_trigger_with_condition() { fn parse_create_instead_of_delete_trigger() { let sql = "CREATE TRIGGER check_delete INSTEAD OF DELETE ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_deletes"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_delete")]), @@ -5231,13 +5236,14 @@ fn parse_create_instead_of_delete_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_deletes")]), args: None, }, - }, + }), + statements: None, characteristics: None, }; @@ -5248,6 +5254,7 @@ fn parse_create_instead_of_delete_trigger() { fn parse_create_trigger_with_multiple_events_and_deferrable() { let sql = "CREATE CONSTRAINT TRIGGER check_multiple_events BEFORE INSERT OR UPDATE OR DELETE ON accounts DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION check_account_changes"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: true, name: ObjectName::from(vec![Ident::new("check_multiple_events")]), @@ -5263,13 +5270,14 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_changes")]), args: None, }, - }, + }), + statements: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Deferred), @@ -5284,6 +5292,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { fn parse_create_trigger_with_referencing() { let sql = "CREATE TRIGGER check_referencing BEFORE INSERT ON accounts REFERENCING NEW TABLE AS new_accounts OLD TABLE AS old_accounts FOR EACH ROW EXECUTE FUNCTION check_account_referencing"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_referencing")]), @@ -5306,13 +5315,14 @@ fn parse_create_trigger_with_referencing() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_referencing")]), args: None, }, - }, + }), + statements: None, characteristics: None, }; @@ -5332,7 +5342,7 @@ fn parse_create_trigger_invalid_cases() { ), ( "CREATE TRIGGER check_update TOMORROW UPDATE ON accounts EXECUTE FUNCTION check_account_update", - "Expected: one of BEFORE or AFTER or INSTEAD, found: TOMORROW" + "Expected: one of FOR or BEFORE or AFTER or INSTEAD, found: TOMORROW" ), ( "CREATE TRIGGER check_update BEFORE SAVE ON accounts EXECUTE FUNCTION check_account_update", @@ -5590,6 +5600,7 @@ fn parse_trigger_related_functions() { assert_eq!( create_trigger, Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), @@ -5601,13 +5612,14 @@ fn parse_trigger_related_functions() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("emp_stamp")]), args: None, } - }, + }), + statements: None, characteristics: None } ); From ac1c339666c68779ed7f20dc0fb3b7473b298f83 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Sun, 4 May 2025 23:21:44 +0200 Subject: [PATCH 056/130] Added support for `CREATE DOMAIN` (#1830) --- src/ast/ddl.rs | 49 +++++++++++++++++++ src/ast/mod.rs | 15 +++--- src/ast/spans.rs | 1 + src/parser/mod.rs | 31 +++++++++++++ tests/sqlparser_postgres.rs | 93 +++++++++++++++++++++++++++++++++++++ 5 files changed, 183 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c1c113b3..a457a065 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2153,6 +2153,55 @@ impl fmt::Display for ClusteredBy { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// ```sql +/// CREATE DOMAIN name [ AS ] data_type +/// [ COLLATE collation ] +/// [ DEFAULT expression ] +/// [ domain_constraint [ ... ] ] +/// +/// where domain_constraint is: +/// +/// [ CONSTRAINT constraint_name ] +/// { NOT NULL | NULL | CHECK (expression) } +/// ``` +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createdomain.html) +pub struct CreateDomain { + /// The name of the domain to be created. + pub name: ObjectName, + /// The data type of the domain. + pub data_type: DataType, + /// The collation of the domain. + pub collation: Option, + /// The default value of the domain. + pub default: Option, + /// The constraints of the domain. + pub constraints: Vec, +} + +impl fmt::Display for CreateDomain { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE DOMAIN {name} AS {data_type}", + name = self.name, + data_type = self.data_type + )?; + if let Some(collation) = &self.collation { + write!(f, " COLLATE {collation}")?; + } + if let Some(default) = &self.default { + write!(f, " DEFAULT {default}")?; + } + if !self.constraints.is_empty() { + write!(f, " {}", display_separated(&self.constraints, " "))?; + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c3009743..a72cb8ad 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -55,12 +55,12 @@ pub use self::ddl::{ AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateConnector, CreateFunction, Deduplicate, DeferrableInitial, - DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexOption, - IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, ProcedureParam, - ReferentialAction, TableConstraint, TagsColumnOption, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, ViewColumnDef, + ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, Deduplicate, + DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, + IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, + IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, + ProcedureParam, ReferentialAction, TableConstraint, TagsColumnOption, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -4049,6 +4049,8 @@ pub enum Statement { sequence_options: Vec, owned_by: Option, }, + /// A `CREATE DOMAIN` statement. + CreateDomain(CreateDomain), /// ```sql /// CREATE TYPE /// ``` @@ -4598,6 +4600,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateFunction(create_function) => create_function.fmt(f), + Statement::CreateDomain(create_domain) => create_domain.fmt(f), Statement::CreateTrigger { or_alter, or_replace, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 3f703ffa..ff2a61cf 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -483,6 +483,7 @@ impl Spanned for Statement { Statement::CreateSchema { .. } => Span::empty(), Statement::CreateDatabase { .. } => Span::empty(), Statement::CreateFunction { .. } => Span::empty(), + Statement::CreateDomain { .. } => Span::empty(), Statement::CreateTrigger { .. } => Span::empty(), Statement::DropTrigger { .. } => Span::empty(), Statement::CreateProcedure { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2011d31e..fc6f4437 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4625,6 +4625,8 @@ impl<'a> Parser<'a> { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { self.parse_create_function(or_alter, or_replace, temporary) + } else if self.parse_keyword(Keyword::DOMAIN) { + self.parse_create_domain() } else if self.parse_keyword(Keyword::TRIGGER) { self.parse_create_trigger(or_alter, or_replace, false) } else if self.parse_keywords(&[Keyword::CONSTRAINT, Keyword::TRIGGER]) { @@ -5974,6 +5976,35 @@ impl<'a> Parser<'a> { Ok(owner) } + /// Parses a [Statement::CreateDomain] statement. + fn parse_create_domain(&mut self) -> Result { + let name = self.parse_object_name(false)?; + self.expect_keyword_is(Keyword::AS)?; + let data_type = self.parse_data_type()?; + let collation = if self.parse_keyword(Keyword::COLLATE) { + Some(self.parse_identifier()?) + } else { + None + }; + let default = if self.parse_keyword(Keyword::DEFAULT) { + Some(self.parse_expr()?) + } else { + None + }; + let mut constraints = Vec::new(); + while let Some(constraint) = self.parse_optional_table_constraint()? { + constraints.push(constraint); + } + + Ok(Statement::CreateDomain(CreateDomain { + name, + data_type, + collation, + default, + constraints, + })) + } + /// ```sql /// CREATE POLICY name ON table_name [ AS { PERMISSIVE | RESTRICTIVE } ] /// [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 008d0670..859eca45 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5153,6 +5153,99 @@ fn test_escaped_string_literal() { } } +#[test] +fn parse_create_domain() { + let sql1 = "CREATE DOMAIN my_domain AS INTEGER CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: None, + default: None, + constraints: vec![TableConstraint::Check { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + }], + }); + + assert_eq!(pg().verified_stmt(sql1), expected); + + let sql2 = "CREATE DOMAIN my_domain AS INTEGER COLLATE \"en_US\" CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: Some(Ident::with_quote('"', "en_US")), + default: None, + constraints: vec![TableConstraint::Check { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + }], + }); + + assert_eq!(pg().verified_stmt(sql2), expected); + + let sql3 = "CREATE DOMAIN my_domain AS INTEGER DEFAULT 1 CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: None, + default: Some(Expr::Value(test_utils::number("1").into())), + constraints: vec![TableConstraint::Check { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + }], + }); + + assert_eq!(pg().verified_stmt(sql3), expected); + + let sql4 = "CREATE DOMAIN my_domain AS INTEGER COLLATE \"en_US\" DEFAULT 1 CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: Some(Ident::with_quote('"', "en_US")), + default: Some(Expr::Value(test_utils::number("1").into())), + constraints: vec![TableConstraint::Check { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + }], + }); + + assert_eq!(pg().verified_stmt(sql4), expected); + + let sql5 = "CREATE DOMAIN my_domain AS INTEGER CONSTRAINT my_constraint CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: None, + default: None, + constraints: vec![TableConstraint::Check { + name: Some(Ident::new("my_constraint")), + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + }], + }); + + assert_eq!(pg().verified_stmt(sql5), expected); +} + #[test] fn parse_create_simple_before_insert_trigger() { let sql = "CREATE TRIGGER check_insert BEFORE INSERT ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_insert"; From 6cd237ea43e5363469418475e61fa503eba2db7b Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 8 May 2025 19:40:03 -0400 Subject: [PATCH 057/130] Allow stored procedures to be defined without `BEGIN`/`END` (#1834) --- src/ast/mod.rs | 9 ++-- src/parser/mod.rs | 8 ++-- tests/sqlparser_mssql.rs | 93 +++++++++++++++++++++------------------- 3 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a72cb8ad..6b7ba12d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3826,7 +3826,7 @@ pub enum Statement { or_alter: bool, name: ObjectName, params: Option>, - body: Vec, + body: ConditionalStatements, }, /// ```sql /// CREATE MACRO @@ -4705,11 +4705,8 @@ impl fmt::Display for Statement { write!(f, " ({})", display_comma_separated(p))?; } } - write!( - f, - " AS BEGIN {body} END", - body = display_separated(body, "; ") - ) + + write!(f, " AS {body}") } Statement::CreateMacro { or_replace, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fc6f4437..d18c7f69 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15505,14 +15505,14 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(false)?; let params = self.parse_optional_procedure_parameters()?; self.expect_keyword_is(Keyword::AS)?; - self.expect_keyword_is(Keyword::BEGIN)?; - let statements = self.parse_statements()?; - self.expect_keyword_is(Keyword::END)?; + + let body = self.parse_conditional_statements(&[Keyword::END])?; + Ok(Statement::CreateProcedure { name, or_alter, params, - body: statements, + body, }) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9ff55198..1c0a00b1 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -100,48 +100,52 @@ fn parse_mssql_delimited_identifiers() { #[test] fn parse_create_procedure() { - let sql = "CREATE OR ALTER PROCEDURE test (@foo INT, @bar VARCHAR(256)) AS BEGIN SELECT 1 END"; + let sql = "CREATE OR ALTER PROCEDURE test (@foo INT, @bar VARCHAR(256)) AS BEGIN SELECT 1; END"; assert_eq!( ms().verified_stmt(sql), Statement::CreateProcedure { or_alter: true, - body: vec![Statement::Query(Box::new(Query { - with: None, - limit_clause: None, - fetch: None, - locks: vec![], - for_clause: None, - order_by: None, - settings: None, - format_clause: None, - pipe_operators: vec![], - body: Box::new(SetExpr::Select(Box::new(Select { - select_token: AttachedToken::empty(), - distinct: None, - top: None, - top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr(Expr::Value( - (number("1")).with_empty_span() - ))], - into: None, - from: vec![], - lateral_views: vec![], - prewhere: None, - selection: None, - group_by: GroupByExpr::Expressions(vec![], vec![]), - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - named_window: vec![], - window_before_qualify: false, - qualify: None, - value_table_mode: None, - connect_by: None, - flavor: SelectFlavor::Standard, - }))) - }))], + body: ConditionalStatements::BeginEnd(BeginEndStatements { + begin_token: AttachedToken::empty(), + statements: vec![Statement::Query(Box::new(Query { + with: None, + limit_clause: None, + fetch: None, + locks: vec![], + for_clause: None, + order_by: None, + settings: None, + format_clause: None, + pipe_operators: vec![], + body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![SelectItem::UnnamedExpr(Expr::Value( + (number("1")).with_empty_span() + ))], + into: None, + from: vec![], + lateral_views: vec![], + prewhere: None, + selection: None, + group_by: GroupByExpr::Expressions(vec![], vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + window_before_qualify: false, + qualify: None, + value_table_mode: None, + connect_by: None, + flavor: SelectFlavor::Standard, + }))) + }))], + end_token: AttachedToken::empty(), + }), params: Some(vec![ ProcedureParam { name: Ident { @@ -174,19 +178,20 @@ fn parse_create_procedure() { #[test] fn parse_mssql_create_procedure() { - let _ = ms_and_generic().verified_stmt("CREATE OR ALTER PROCEDURE foo AS BEGIN SELECT 1 END"); - let _ = ms_and_generic().verified_stmt("CREATE PROCEDURE foo AS BEGIN SELECT 1 END"); + let _ = ms_and_generic().verified_stmt("CREATE OR ALTER PROCEDURE foo AS SELECT 1;"); + let _ = ms_and_generic().verified_stmt("CREATE OR ALTER PROCEDURE foo AS BEGIN SELECT 1; END"); + let _ = ms_and_generic().verified_stmt("CREATE PROCEDURE foo AS BEGIN SELECT 1; END"); let _ = ms().verified_stmt( - "CREATE PROCEDURE foo AS BEGIN SELECT [myColumn] FROM [myschema].[mytable] END", + "CREATE PROCEDURE foo AS BEGIN SELECT [myColumn] FROM [myschema].[mytable]; END", ); let _ = ms_and_generic().verified_stmt( - "CREATE PROCEDURE foo (@CustomerName NVARCHAR(50)) AS BEGIN SELECT * FROM DEV END", + "CREATE PROCEDURE foo (@CustomerName NVARCHAR(50)) AS BEGIN SELECT * FROM DEV; END", ); - let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test' END"); + let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; END"); // Test a statement with END in it - let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN SELECT [foo], CASE WHEN [foo] IS NULL THEN 'empty' ELSE 'notempty' END AS [foo] END"); + let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN SELECT [foo], CASE WHEN [foo] IS NULL THEN 'empty' ELSE 'notempty' END AS [foo]; END"); // Multiple statements - let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; SELECT [foo] FROM BAR WHERE [FOO] > 10 END"); + let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; SELECT [foo] FROM BAR WHERE [FOO] > 10; END"); } #[test] From 2182f7ea71242a7e9674932d559baef67dae522d Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Fri, 9 May 2025 01:48:23 +0200 Subject: [PATCH 058/130] Add support for the MATCH and REGEXP binary operators (#1840) --- src/ast/operator.rs | 7 +++++++ src/dialect/mod.rs | 2 ++ src/dialect/sqlite.rs | 27 ++++++++++++++++++++++++++- tests/sqlparser_sqlite.rs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 73fe9cf4..d0bb05e3 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -139,6 +139,11 @@ pub enum BinaryOperator { DuckIntegerDivide, /// MySQL [`DIV`](https://dev.mysql.com/doc/refman/8.0/en/arithmetic-functions.html) integer division MyIntegerDivide, + /// MATCH operator, e.g. `a MATCH b` (SQLite-specific) + /// See + Match, + /// REGEXP operator, e.g. `a REGEXP b` (SQLite-specific) + Regexp, /// Support for custom operators (such as Postgres custom operators) Custom(String), /// Bitwise XOR, e.g. `a # b` (PostgreSQL-specific) @@ -350,6 +355,8 @@ impl fmt::Display for BinaryOperator { BinaryOperator::BitwiseXor => f.write_str("^"), BinaryOperator::DuckIntegerDivide => f.write_str("//"), BinaryOperator::MyIntegerDivide => f.write_str("DIV"), + BinaryOperator::Match => f.write_str("MATCH"), + BinaryOperator::Regexp => f.write_str("REGEXP"), BinaryOperator::Custom(s) => f.write_str(s), BinaryOperator::PGBitwiseXor => f.write_str("#"), BinaryOperator::PGBitwiseShiftLeft => f.write_str("<<"), diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b754a04f..6fbbc7a2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -619,6 +619,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), _ => Ok(self.prec_unknown()), }, @@ -630,6 +631,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)), diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 138c4692..847e0d13 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -15,7 +15,11 @@ // specific language governing permissions and limitations // under the License. -use crate::ast::Statement; +#[cfg(not(feature = "std"))] +use alloc::boxed::Box; + +use crate::ast::BinaryOperator; +use crate::ast::{Expr, Statement}; use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -70,6 +74,27 @@ impl Dialect for SQLiteDialect { } } + fn parse_infix( + &self, + parser: &mut crate::parser::Parser, + expr: &crate::ast::Expr, + _precedence: u8, + ) -> Option> { + // Parse MATCH and REGEXP as operators + // See + for (keyword, op) in [ + (Keyword::REGEXP, BinaryOperator::Regexp), + (Keyword::MATCH, BinaryOperator::Match), + ] { + if parser.parse_keyword(keyword) { + let left = Box::new(expr.clone()); + let right = Box::new(parser.parse_expr().unwrap()); + return Some(Ok(Expr::BinaryOp { left, op, right })); + } + } + None + } + fn supports_in_empty_list(&self) -> bool { true } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 361c9b05..b759065f 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -562,6 +562,36 @@ fn test_dollar_identifier_as_placeholder() { } } +#[test] +fn test_match_operator() { + assert_eq!( + sqlite().verified_expr("col MATCH 'pattern'"), + Expr::BinaryOp { + op: BinaryOperator::Match, + left: Box::new(Expr::Identifier(Ident::new("col"))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("pattern".to_string())).with_empty_span() + )) + } + ); + sqlite().verified_only_select("SELECT * FROM email WHERE email MATCH 'fts5'"); +} + +#[test] +fn test_regexp_operator() { + assert_eq!( + sqlite().verified_expr("col REGEXP 'pattern'"), + Expr::BinaryOp { + op: BinaryOperator::Regexp, + left: Box::new(Expr::Identifier(Ident::new("col"))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("pattern".to_string())).with_empty_span() + )) + } + ); + sqlite().verified_only_select(r#"SELECT count(*) FROM messages WHERE msg_text REGEXP '\d+'"#); +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) } From 052ad4a75981f1d411f8440e618217c7f010bbb9 Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Sat, 10 May 2025 01:14:25 +0100 Subject: [PATCH 059/130] Fix: parsing ident starting with underscore in certain dialects (#1835) --- src/tokenizer.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 4fad5462..afe1e35c 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1191,6 +1191,22 @@ impl<'a> Tokenizer<'a> { } // numbers and period '0'..='9' | '.' => { + // special case where if ._ is encountered after a word then that word + // is a table and the _ is the start of the col name. + // if the prev token is not a word, then this is not a valid sql + // word or number. + if ch == '.' && chars.peekable.clone().nth(1) == Some('_') { + if let Some(Token::Word(_)) = prev_token { + chars.next(); + return Ok(Some(Token::Period)); + } + + return self.tokenizer_error( + chars.location(), + "Unexpected character '_'".to_string(), + ); + } + // Some dialects support underscore as number separator // There can only be one at a time and it must be followed by another digit let is_number_separator = |ch: char, next_char: Option| { @@ -4018,4 +4034,40 @@ mod tests { ], ); } + + #[test] + fn tokenize_period_underscore() { + let sql = String::from("SELECT table._col"); + // a dialect that supports underscores in numeric literals + let dialect = PostgreSqlDialect {}; + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Word(Word { + value: "table".to_string(), + quote_style: None, + keyword: Keyword::TABLE, + }), + Token::Period, + Token::Word(Word { + value: "_col".to_string(), + quote_style: None, + keyword: Keyword::NoKeyword, + }), + ]; + + compare(expected, tokens); + + let sql = String::from("SELECT ._123"); + if let Ok(tokens) = Tokenizer::new(&dialect, &sql).tokenize() { + panic!("Tokenizer should have failed on {sql}, but it succeeded with {tokens:?}"); + } + + let sql = String::from("SELECT ._abc"); + if let Ok(tokens) = Tokenizer::new(&dialect, &sql).tokenize() { + panic!("Tokenizer should have failed on {sql}, but it succeeded with {tokens:?}"); + } + } } From 6120bb59cc9cab0403366d644e914945155031a2 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 13 May 2025 15:25:07 +0200 Subject: [PATCH 060/130] implement pretty-printing with `{:#}` (#1847) --- src/ast/mod.rs | 102 ++++++++++--- src/ast/query.rs | 337 +++++++++++++++++++++++++----------------- src/display_utils.rs | 133 +++++++++++++++++ src/lib.rs | 22 +++ tests/pretty_print.rs | 157 ++++++++++++++++++++ 5 files changed, 591 insertions(+), 160 deletions(-) create mode 100644 src/display_utils.rs create mode 100644 tests/pretty_print.rs diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6b7ba12d..79b0e0d5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,8 +40,14 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::keywords::Keyword; -use crate::tokenizer::{Span, Token}; +use crate::{ + display_utils::SpaceOrNewline, + tokenizer::{Span, Token}, +}; +use crate::{ + display_utils::{Indent, NewLine}, + keywords::Keyword, +}; pub use self::data_type::{ ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember, @@ -134,9 +140,9 @@ where fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut delim = ""; for t in self.slice { - write!(f, "{delim}")?; + f.write_str(delim)?; delim = self.sep; - write!(f, "{t}")?; + t.fmt(f)?; } Ok(()) } @@ -628,7 +634,12 @@ pub struct CaseWhen { impl fmt::Display for CaseWhen { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "WHEN {} THEN {}", self.condition, self.result) + f.write_str("WHEN ")?; + self.condition.fmt(f)?; + f.write_str(" THEN")?; + SpaceOrNewline.fmt(f)?; + Indent(&self.result).fmt(f)?; + Ok(()) } } @@ -1662,23 +1673,29 @@ impl fmt::Display for Expr { write!(f, "{data_type}")?; write!(f, " {value}") } - Expr::Function(fun) => write!(f, "{fun}"), + Expr::Function(fun) => fun.fmt(f), Expr::Case { operand, conditions, else_result, } => { - write!(f, "CASE")?; + f.write_str("CASE")?; if let Some(operand) = operand { - write!(f, " {operand}")?; + f.write_str(" ")?; + operand.fmt(f)?; } for when in conditions { - write!(f, " {when}")?; + SpaceOrNewline.fmt(f)?; + Indent(when).fmt(f)?; } if let Some(else_result) = else_result { - write!(f, " ELSE {else_result}")?; + SpaceOrNewline.fmt(f)?; + Indent("ELSE").fmt(f)?; + SpaceOrNewline.fmt(f)?; + Indent(Indent(else_result)).fmt(f)?; } - write!(f, " END") + SpaceOrNewline.fmt(f)?; + f.write_str("END") } Expr::Exists { subquery, negated } => write!( f, @@ -1867,8 +1884,14 @@ pub enum WindowType { impl Display for WindowType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - WindowType::WindowSpec(spec) => write!(f, "({})", spec), - WindowType::NamedWindow(name) => write!(f, "{}", name), + WindowType::WindowSpec(spec) => { + f.write_str("(")?; + NewLine.fmt(f)?; + Indent(spec).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")") + } + WindowType::NamedWindow(name) => name.fmt(f), } } } @@ -1896,14 +1919,19 @@ pub struct WindowSpec { impl fmt::Display for WindowSpec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut delim = ""; + let mut is_first = true; if let Some(window_name) = &self.window_name { - delim = " "; + if !is_first { + SpaceOrNewline.fmt(f)?; + } + is_first = false; write!(f, "{window_name}")?; } if !self.partition_by.is_empty() { - f.write_str(delim)?; - delim = " "; + if !is_first { + SpaceOrNewline.fmt(f)?; + } + is_first = false; write!( f, "PARTITION BY {}", @@ -1911,12 +1939,16 @@ impl fmt::Display for WindowSpec { )?; } if !self.order_by.is_empty() { - f.write_str(delim)?; - delim = " "; + if !is_first { + SpaceOrNewline.fmt(f)?; + } + is_first = false; write!(f, "ORDER BY {}", display_comma_separated(&self.order_by))?; } if let Some(window_frame) = &self.window_frame { - f.write_str(delim)?; + if !is_first { + SpaceOrNewline.fmt(f)?; + } if let Some(end_bound) = &window_frame.end_bound { write!( f, @@ -4204,6 +4236,28 @@ impl fmt::Display for RaisErrorOption { } impl fmt::Display for Statement { + /// Formats a SQL statement with support for pretty printing. + /// + /// When using the alternate flag (`{:#}`), the statement will be formatted with proper + /// indentation and line breaks. For example: + /// + /// ``` + /// # use sqlparser::dialect::GenericDialect; + /// # use sqlparser::parser::Parser; + /// let sql = "SELECT a, b FROM table_1"; + /// let ast = Parser::parse_sql(&GenericDialect, sql).unwrap(); + /// + /// // Regular formatting + /// assert_eq!(format!("{}", ast[0]), "SELECT a, b FROM table_1"); + /// + /// // Pretty printing + /// assert_eq!(format!("{:#}", ast[0]), + /// r#"SELECT + /// a, + /// b + /// FROM + /// table_1"#); + /// ``` // Clippy thinks this function is too complicated, but it is painful to // split up without extracting structs for each `Statement` variant. #[allow(clippy::cognitive_complexity)] @@ -4219,7 +4273,8 @@ impl fmt::Display for Statement { } => { write!(f, "FLUSH")?; if let Some(location) = location { - write!(f, " {location}")?; + f.write_str(" ")?; + location.fmt(f)?; } write!(f, " {object_type}")?; @@ -4301,7 +4356,7 @@ impl fmt::Display for Statement { write!(f, "{statement}") } - Statement::Query(s) => write!(f, "{s}"), + Statement::Query(s) => s.fmt(f), Statement::Declare { stmts } => { write!(f, "DECLARE ")?; write!(f, "{}", display_separated(stmts, "; ")) @@ -7056,7 +7111,8 @@ impl fmt::Display for Function { } if let Some(o) = &self.over { - write!(f, " OVER {o}")?; + f.write_str(" OVER ")?; + o.fmt(f)?; } if self.uses_odbc_syntax { diff --git a/src/ast/query.rs b/src/ast/query.rs index a90b6166..33168695 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -27,6 +27,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::{ ast::*, + display_utils::{indented_list, SpaceOrNewline}, tokenizer::{Token, TokenWithSpan}, }; @@ -70,33 +71,41 @@ pub struct Query { impl fmt::Display for Query { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(ref with) = self.with { - write!(f, "{with} ")?; + with.fmt(f)?; + SpaceOrNewline.fmt(f)?; } - write!(f, "{}", self.body)?; + self.body.fmt(f)?; if let Some(ref order_by) = self.order_by { - write!(f, " {order_by}")?; + f.write_str(" ")?; + order_by.fmt(f)?; } if let Some(ref limit_clause) = self.limit_clause { limit_clause.fmt(f)?; } if let Some(ref settings) = self.settings { - write!(f, " SETTINGS {}", display_comma_separated(settings))?; + f.write_str(" SETTINGS ")?; + display_comma_separated(settings).fmt(f)?; } if let Some(ref fetch) = self.fetch { - write!(f, " {fetch}")?; + f.write_str(" ")?; + fetch.fmt(f)?; } if !self.locks.is_empty() { - write!(f, " {}", display_separated(&self.locks, " "))?; + f.write_str(" ")?; + display_separated(&self.locks, " ").fmt(f)?; } if let Some(ref for_clause) = self.for_clause { - write!(f, " {}", for_clause)?; + f.write_str(" ")?; + for_clause.fmt(f)?; } if let Some(ref format) = self.format_clause { - write!(f, " {}", format)?; + f.write_str(" ")?; + format.fmt(f)?; } for pipe_operator in &self.pipe_operators { - write!(f, " |> {}", pipe_operator)?; + f.write_str(" |> ")?; + pipe_operator.fmt(f)?; } Ok(()) } @@ -169,29 +178,39 @@ impl SetExpr { impl fmt::Display for SetExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - SetExpr::Select(s) => write!(f, "{s}"), - SetExpr::Query(q) => write!(f, "({q})"), - SetExpr::Values(v) => write!(f, "{v}"), - SetExpr::Insert(v) => write!(f, "{v}"), - SetExpr::Update(v) => write!(f, "{v}"), - SetExpr::Delete(v) => write!(f, "{v}"), - SetExpr::Table(t) => write!(f, "{t}"), + SetExpr::Select(s) => s.fmt(f), + SetExpr::Query(q) => { + f.write_str("(")?; + q.fmt(f)?; + f.write_str(")") + } + SetExpr::Values(v) => v.fmt(f), + SetExpr::Insert(v) => v.fmt(f), + SetExpr::Update(v) => v.fmt(f), + SetExpr::Delete(v) => v.fmt(f), + SetExpr::Table(t) => t.fmt(f), SetExpr::SetOperation { left, right, op, set_quantifier, } => { - write!(f, "{left} {op}")?; + left.fmt(f)?; + SpaceOrNewline.fmt(f)?; + op.fmt(f)?; match set_quantifier { SetQuantifier::All | SetQuantifier::Distinct | SetQuantifier::ByName | SetQuantifier::AllByName - | SetQuantifier::DistinctByName => write!(f, " {set_quantifier}")?, - SetQuantifier::None => write!(f, "{set_quantifier}")?, + | SetQuantifier::DistinctByName => { + f.write_str(" ")?; + set_quantifier.fmt(f)?; + } + SetQuantifier::None => {} } - write!(f, " {right}")?; + SpaceOrNewline.fmt(f)?; + right.fmt(f)?; Ok(()) } } @@ -242,7 +261,7 @@ impl fmt::Display for SetQuantifier { SetQuantifier::ByName => write!(f, "BY NAME"), SetQuantifier::AllByName => write!(f, "ALL BY NAME"), SetQuantifier::DistinctByName => write!(f, "DISTINCT BY NAME"), - SetQuantifier::None => write!(f, ""), + SetQuantifier::None => Ok(()), } } } @@ -357,90 +376,122 @@ impl fmt::Display for Select { } if let Some(value_table_mode) = self.value_table_mode { - write!(f, " {value_table_mode}")?; + f.write_str(" ")?; + value_table_mode.fmt(f)?; } if let Some(ref top) = self.top { if self.top_before_distinct { - write!(f, " {top}")?; + f.write_str(" ")?; + top.fmt(f)?; } } if let Some(ref distinct) = self.distinct { - write!(f, " {distinct}")?; + f.write_str(" ")?; + distinct.fmt(f)?; } if let Some(ref top) = self.top { if !self.top_before_distinct { - write!(f, " {top}")?; + f.write_str(" ")?; + top.fmt(f)?; } } if !self.projection.is_empty() { - write!(f, " {}", display_comma_separated(&self.projection))?; + indented_list(f, &self.projection)?; } if let Some(ref into) = self.into { - write!(f, " {into}")?; + f.write_str(" ")?; + into.fmt(f)?; } if self.flavor == SelectFlavor::Standard && !self.from.is_empty() { - write!(f, " FROM {}", display_comma_separated(&self.from))?; + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, &self.from)?; } if !self.lateral_views.is_empty() { for lv in &self.lateral_views { - write!(f, "{lv}")?; + lv.fmt(f)?; } } if let Some(ref prewhere) = self.prewhere { - write!(f, " PREWHERE {prewhere}")?; + f.write_str(" PREWHERE ")?; + prewhere.fmt(f)?; } if let Some(ref selection) = self.selection { - write!(f, " WHERE {selection}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; } match &self.group_by { - GroupByExpr::All(_) => write!(f, " {}", self.group_by)?, + GroupByExpr::All(_) => { + SpaceOrNewline.fmt(f)?; + self.group_by.fmt(f)?; + } GroupByExpr::Expressions(exprs, _) => { if !exprs.is_empty() { - write!(f, " {}", self.group_by)? + SpaceOrNewline.fmt(f)?; + self.group_by.fmt(f)?; } } } if !self.cluster_by.is_empty() { - write!( - f, - " CLUSTER BY {}", - display_comma_separated(&self.cluster_by) - )?; + SpaceOrNewline.fmt(f)?; + f.write_str("CLUSTER BY")?; + SpaceOrNewline.fmt(f)?; + Indent(display_comma_separated(&self.cluster_by)).fmt(f)?; } if !self.distribute_by.is_empty() { - write!( - f, - " DISTRIBUTE BY {}", - display_comma_separated(&self.distribute_by) - )?; + SpaceOrNewline.fmt(f)?; + f.write_str("DISTRIBUTE BY")?; + SpaceOrNewline.fmt(f)?; + display_comma_separated(&self.distribute_by).fmt(f)?; } if !self.sort_by.is_empty() { - write!(f, " SORT BY {}", display_comma_separated(&self.sort_by))?; + SpaceOrNewline.fmt(f)?; + f.write_str("SORT BY")?; + SpaceOrNewline.fmt(f)?; + Indent(display_comma_separated(&self.sort_by)).fmt(f)?; } if let Some(ref having) = self.having { - write!(f, " HAVING {having}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("HAVING")?; + SpaceOrNewline.fmt(f)?; + Indent(having).fmt(f)?; } if self.window_before_qualify { if !self.named_window.is_empty() { - write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?; + SpaceOrNewline.fmt(f)?; + f.write_str("WINDOW")?; + SpaceOrNewline.fmt(f)?; + display_comma_separated(&self.named_window).fmt(f)?; } if let Some(ref qualify) = self.qualify { - write!(f, " QUALIFY {qualify}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("QUALIFY")?; + SpaceOrNewline.fmt(f)?; + qualify.fmt(f)?; } } else { if let Some(ref qualify) = self.qualify { - write!(f, " QUALIFY {qualify}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("QUALIFY")?; + SpaceOrNewline.fmt(f)?; + qualify.fmt(f)?; } if !self.named_window.is_empty() { - write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?; + SpaceOrNewline.fmt(f)?; + f.write_str("WINDOW")?; + SpaceOrNewline.fmt(f)?; + display_comma_separated(&self.named_window).fmt(f)?; } } if let Some(ref connect_by) = self.connect_by { - write!(f, " {connect_by}")?; + SpaceOrNewline.fmt(f)?; + connect_by.fmt(f)?; } Ok(()) } @@ -546,12 +597,12 @@ pub struct With { impl fmt::Display for With { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "WITH {}{}", - if self.recursive { "RECURSIVE " } else { "" }, - display_comma_separated(&self.cte_tables) - ) + f.write_str("WITH ")?; + if self.recursive { + f.write_str("RECURSIVE ")?; + } + display_comma_separated(&self.cte_tables).fmt(f)?; + Ok(()) } } @@ -598,8 +649,24 @@ pub struct Cte { impl fmt::Display for Cte { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.materialized.as_ref() { - None => write!(f, "{} AS ({})", self.alias, self.query)?, - Some(materialized) => write!(f, "{} AS {materialized} ({})", self.alias, self.query)?, + None => { + self.alias.fmt(f)?; + f.write_str(" AS (")?; + NewLine.fmt(f)?; + Indent(&self.query).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; + } + Some(materialized) => { + self.alias.fmt(f)?; + f.write_str(" AS ")?; + materialized.fmt(f)?; + f.write_str(" (")?; + NewLine.fmt(f)?; + Indent(&self.query).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; + } }; if let Some(ref fr) = self.from { write!(f, " FROM {fr}")?; @@ -912,18 +979,21 @@ impl fmt::Display for ReplaceSelectElement { impl fmt::Display for SelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use core::fmt::Write; match &self { - SelectItem::UnnamedExpr(expr) => write!(f, "{expr}"), - SelectItem::ExprWithAlias { expr, alias } => write!(f, "{expr} AS {alias}"), + SelectItem::UnnamedExpr(expr) => expr.fmt(f), + SelectItem::ExprWithAlias { expr, alias } => { + expr.fmt(f)?; + f.write_str(" AS ")?; + alias.fmt(f) + } SelectItem::QualifiedWildcard(kind, additional_options) => { - write!(f, "{kind}")?; - write!(f, "{additional_options}")?; - Ok(()) + kind.fmt(f)?; + additional_options.fmt(f) } SelectItem::Wildcard(additional_options) => { - write!(f, "*")?; - write!(f, "{additional_options}")?; - Ok(()) + f.write_char('*')?; + additional_options.fmt(f) } } } @@ -939,9 +1009,10 @@ pub struct TableWithJoins { impl fmt::Display for TableWithJoins { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.relation)?; + self.relation.fmt(f)?; for join in &self.joins { - write!(f, "{join}")?; + SpaceOrNewline.fmt(f)?; + join.fmt(f)?; } Ok(()) } @@ -1334,7 +1405,6 @@ pub enum TableFactor { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] - pub enum TableSampleKind { /// Table sample located before the table alias option BeforeTableAlias(Box), @@ -1769,9 +1839,9 @@ impl fmt::Display for TableFactor { sample, index_hints, } => { - write!(f, "{name}")?; + name.fmt(f)?; if let Some(json_path) = json_path { - write!(f, "{json_path}")?; + json_path.fmt(f)?; } if !partitions.is_empty() { write!(f, "PARTITION ({})", display_comma_separated(partitions))?; @@ -1818,7 +1888,11 @@ impl fmt::Display for TableFactor { if *lateral { write!(f, "LATERAL ")?; } - write!(f, "({subquery})")?; + f.write_str("(")?; + NewLine.fmt(f)?; + Indent(subquery).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; if let Some(alias) = alias { write!(f, " AS {alias}")?; } @@ -2132,116 +2206,104 @@ impl fmt::Display for Join { Suffix(constraint) } if self.global { - write!(f, " GLOBAL")?; + write!(f, "GLOBAL ")?; } match &self.join_operator { - JoinOperator::Join(constraint) => write!( - f, - " {}JOIN {}{}", + JoinOperator::Join(constraint) => f.write_fmt(format_args!( + "{}JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Inner(constraint) => write!( - f, - " {}INNER JOIN {}{}", + )), + JoinOperator::Inner(constraint) => f.write_fmt(format_args!( + "{}INNER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Left(constraint) => write!( - f, - " {}LEFT JOIN {}{}", + )), + JoinOperator::Left(constraint) => f.write_fmt(format_args!( + "{}LEFT JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftOuter(constraint) => write!( - f, - " {}LEFT OUTER JOIN {}{}", + )), + JoinOperator::LeftOuter(constraint) => f.write_fmt(format_args!( + "{}LEFT OUTER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Right(constraint) => write!( - f, - " {}RIGHT JOIN {}{}", + )), + JoinOperator::Right(constraint) => f.write_fmt(format_args!( + "{}RIGHT JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightOuter(constraint) => write!( - f, - " {}RIGHT OUTER JOIN {}{}", + )), + JoinOperator::RightOuter(constraint) => f.write_fmt(format_args!( + "{}RIGHT OUTER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::FullOuter(constraint) => write!( - f, - " {}FULL JOIN {}{}", + )), + JoinOperator::FullOuter(constraint) => f.write_fmt(format_args!( + "{}FULL JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::CrossJoin => write!(f, " CROSS JOIN {}", self.relation), - JoinOperator::Semi(constraint) => write!( - f, - " {}SEMI JOIN {}{}", + )), + JoinOperator::CrossJoin => f.write_fmt(format_args!("CROSS JOIN {}", self.relation)), + JoinOperator::Semi(constraint) => f.write_fmt(format_args!( + "{}SEMI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftSemi(constraint) => write!( - f, - " {}LEFT SEMI JOIN {}{}", + )), + JoinOperator::LeftSemi(constraint) => f.write_fmt(format_args!( + "{}LEFT SEMI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightSemi(constraint) => write!( - f, - " {}RIGHT SEMI JOIN {}{}", + )), + JoinOperator::RightSemi(constraint) => f.write_fmt(format_args!( + "{}RIGHT SEMI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Anti(constraint) => write!( - f, - " {}ANTI JOIN {}{}", + )), + JoinOperator::Anti(constraint) => f.write_fmt(format_args!( + "{}ANTI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftAnti(constraint) => write!( - f, - " {}LEFT ANTI JOIN {}{}", + )), + JoinOperator::LeftAnti(constraint) => f.write_fmt(format_args!( + "{}LEFT ANTI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightAnti(constraint) => write!( - f, - " {}RIGHT ANTI JOIN {}{}", + )), + JoinOperator::RightAnti(constraint) => f.write_fmt(format_args!( + "{}RIGHT ANTI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::CrossApply => write!(f, " CROSS APPLY {}", self.relation), - JoinOperator::OuterApply => write!(f, " OUTER APPLY {}", self.relation), + )), + JoinOperator::CrossApply => f.write_fmt(format_args!("CROSS APPLY {}", self.relation)), + JoinOperator::OuterApply => f.write_fmt(format_args!("OUTER APPLY {}", self.relation)), JoinOperator::AsOf { match_condition, constraint, - } => write!( - f, - " ASOF JOIN {} MATCH_CONDITION ({match_condition}){}", + } => f.write_fmt(format_args!( + "ASOF JOIN {} MATCH_CONDITION ({match_condition}){}", self.relation, suffix(constraint) - ), - JoinOperator::StraightJoin(constraint) => { - write!(f, " STRAIGHT_JOIN {}{}", self.relation, suffix(constraint)) - } + )), + JoinOperator::StraightJoin(constraint) => f.write_fmt(format_args!( + "STRAIGHT_JOIN {}{}", + self.relation, + suffix(constraint) + )), } } } @@ -2914,8 +2976,9 @@ impl fmt::Display for GroupByExpr { Ok(()) } GroupByExpr::Expressions(col_names, modifiers) => { - let col_names = display_comma_separated(col_names); - write!(f, "GROUP BY {col_names}")?; + f.write_str("GROUP BY")?; + SpaceOrNewline.fmt(f)?; + Indent(display_comma_separated(col_names)).fmt(f)?; if !modifiers.is_empty() { write!(f, " {}", display_separated(modifiers, " "))?; } diff --git a/src/display_utils.rs b/src/display_utils.rs new file mode 100644 index 00000000..e7e1272f --- /dev/null +++ b/src/display_utils.rs @@ -0,0 +1,133 @@ +//! Utilities for formatting SQL AST nodes with pretty printing support. +//! +//! The module provides formatters that implement the `Display` trait with support +//! for both regular (`{}`) and pretty (`{:#}`) formatting modes. Pretty printing +//! adds proper indentation and line breaks to make SQL statements more readable. + +use core::fmt::{self, Display, Write}; + +/// A wrapper around a value that adds an indent to the value when displayed with {:#}. +pub(crate) struct Indent(pub T); + +const INDENT: &str = " "; + +impl Display for Indent +where + T: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + f.write_str(INDENT)?; + write!(Indent(f), "{:#}", self.0) + } else { + self.0.fmt(f) + } + } +} + +/// Adds an indent to the inner writer +impl Write for Indent +where + T: Write, +{ + fn write_str(&mut self, s: &str) -> fmt::Result { + let mut first = true; + for line in s.split('\n') { + if !first { + write!(self.0, "\n{INDENT}")?; + } + self.0.write_str(line)?; + first = false; + } + Ok(()) + } +} + +/// A value that inserts a newline when displayed with {:#}, but not when displayed with {}. +pub(crate) struct NewLine; + +impl Display for NewLine { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + f.write_char('\n') + } else { + Ok(()) + } + } +} + +/// A value that inserts a space when displayed with {}, but a newline when displayed with {:#}. +pub(crate) struct SpaceOrNewline; + +impl Display for SpaceOrNewline { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + f.write_char('\n') + } else { + f.write_char(' ') + } + } +} + +/// A value that displays a comma-separated list of values. +/// When pretty-printed (using {:#}), it displays each value on a new line. +pub struct DisplayCommaSeparated<'a, T: fmt::Display>(&'a [T]); + +impl fmt::Display for DisplayCommaSeparated<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut first = true; + for t in self.0 { + if !first { + f.write_char(',')?; + SpaceOrNewline.fmt(f)?; + } + first = false; + t.fmt(f)?; + } + Ok(()) + } +} + +/// Displays a whitespace, followed by a comma-separated list that is indented when pretty-printed. +pub(crate) fn indented_list(f: &mut fmt::Formatter, slice: &[T]) -> fmt::Result { + SpaceOrNewline.fmt(f)?; + Indent(DisplayCommaSeparated(slice)).fmt(f) +} + +#[cfg(test)] +mod tests { + use super::*; + + struct DisplayCharByChar(T); + + impl Display for DisplayCharByChar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for c in self.0.to_string().chars() { + write!(f, "{}", c)?; + } + Ok(()) + } + } + + #[test] + fn test_indent() { + let original = "line 1\nline 2"; + let indent = Indent(original); + assert_eq!( + indent.to_string(), + original, + "Only the alternate form should be indented" + ); + let expected = " line 1\n line 2"; + assert_eq!(format!("{:#}", indent), expected); + let display_char_by_char = DisplayCharByChar(original); + assert_eq!(format!("{:#}", Indent(display_char_by_char)), expected); + } + + #[test] + fn test_space_or_newline() { + let space_or_newline = SpaceOrNewline; + assert_eq!(format!("{}", space_or_newline), " "); + assert_eq!(format!("{:#}", space_or_newline), "\n"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5d72f9f0..c81ab500 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,27 @@ //! // The original SQL text can be generated from the AST //! assert_eq!(ast[0].to_string(), sql); //! ``` +//! +//! # Pretty Printing +//! +//! SQL statements can be pretty-printed with proper indentation and line breaks using the alternate flag (`{:#}`): +//! +//! ``` +//! # use sqlparser::dialect::GenericDialect; +//! # use sqlparser::parser::Parser; +//! let sql = "SELECT a, b FROM table_1"; +//! let ast = Parser::parse_sql(&GenericDialect, sql).unwrap(); +//! +//! // Pretty print with indentation and line breaks +//! let pretty_sql = format!("{:#}", ast[0]); +//! assert_eq!(pretty_sql, r#" +//! SELECT +//! a, +//! b +//! FROM +//! table_1 +//! "#.trim()); +//! ``` //! [sqlparser crates.io page]: https://crates.io/crates/sqlparser //! [`Parser::parse_sql`]: crate::parser::Parser::parse_sql //! [`Parser::new`]: crate::parser::Parser::new @@ -142,6 +163,7 @@ extern crate pretty_assertions; pub mod ast; #[macro_use] pub mod dialect; +mod display_utils; pub mod keywords; pub mod parser; pub mod tokenizer; diff --git a/tests/pretty_print.rs b/tests/pretty_print.rs new file mode 100644 index 00000000..1eb8ca41 --- /dev/null +++ b/tests/pretty_print.rs @@ -0,0 +1,157 @@ +use sqlparser::dialect::GenericDialect; +use sqlparser::parser::Parser; + +fn prettify(sql: &str) -> String { + let ast = Parser::parse_sql(&GenericDialect {}, sql).unwrap(); + format!("{:#}", ast[0]) +} + +#[test] +fn test_pretty_print_select() { + assert_eq!( + prettify("SELECT a, b, c FROM my_table WHERE x = 1 AND y = 2"), + r#" +SELECT + a, + b, + c +FROM + my_table +WHERE + x = 1 AND y = 2 +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_join() { + assert_eq!( + prettify("SELECT a FROM table1 JOIN table2 ON table1.id = table2.id"), + r#" +SELECT + a +FROM + table1 + JOIN table2 ON table1.id = table2.id +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_subquery() { + assert_eq!( + prettify("SELECT * FROM (SELECT a, b FROM my_table) AS subquery"), + r#" +SELECT + * +FROM + ( + SELECT + a, + b + FROM + my_table + ) AS subquery +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_union() { + assert_eq!( + prettify("SELECT a FROM table1 UNION SELECT b FROM table2"), + r#" +SELECT + a +FROM + table1 +UNION +SELECT + b +FROM + table2 +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_group_by() { + assert_eq!( + prettify("SELECT a, COUNT(*) FROM my_table GROUP BY a HAVING COUNT(*) > 1"), + r#" +SELECT + a, + COUNT(*) +FROM + my_table +GROUP BY + a +HAVING + COUNT(*) > 1 +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_cte() { + assert_eq!( + prettify("WITH cte AS (SELECT a, b FROM my_table) SELECT * FROM cte"), + r#" +WITH cte AS ( + SELECT + a, + b + FROM + my_table +) +SELECT + * +FROM + cte +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_case_when() { + assert_eq!( + prettify("SELECT CASE WHEN x > 0 THEN 'positive' WHEN x < 0 THEN 'negative' ELSE 'zero' END FROM my_table"), + r#" +SELECT + CASE + WHEN x > 0 THEN + 'positive' + WHEN x < 0 THEN + 'negative' + ELSE + 'zero' + END +FROM + my_table +"#.trim() + ); +} + +#[test] +fn test_pretty_print_window_function() { + assert_eq!( + prettify("SELECT id, value, ROW_NUMBER() OVER (PARTITION BY category ORDER BY value DESC) as rank FROM my_table"), + r#" +SELECT + id, + value, + ROW_NUMBER() OVER ( + PARTITION BY category + ORDER BY value DESC + ) AS rank +FROM + my_table +"#.trim() + ); +} From 178a351812e34f8ca9cf0e6662f9e717addebebb Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 13 May 2025 17:36:27 +0200 Subject: [PATCH 061/130] Fix big performance issue in string serialization (#1848) --- src/ast/value.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index 77e2e0e8..98616407 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -456,30 +456,38 @@ impl fmt::Display for EscapeQuotedString<'_> { // | `"A\"B\"A"` | default | `DoubleQuotedString(String::from("A\"B\"A"))` | `"A""B""A"` | let quote = self.quote; let mut previous_char = char::default(); - let mut peekable_chars = self.string.chars().peekable(); - while let Some(&ch) = peekable_chars.peek() { + let mut start_idx = 0; + let mut peekable_chars = self.string.char_indices().peekable(); + while let Some(&(idx, ch)) = peekable_chars.peek() { match ch { char if char == quote => { if previous_char == '\\' { - write!(f, "{char}")?; + // the quote is already escaped with a backslash, skip peekable_chars.next(); continue; } peekable_chars.next(); - if peekable_chars.peek().map(|c| *c == quote).unwrap_or(false) { - write!(f, "{char}{char}")?; - peekable_chars.next(); - } else { - write!(f, "{char}{char}")?; + match peekable_chars.peek() { + Some((_, c)) if *c == quote => { + // the quote is already escaped with another quote, skip + peekable_chars.next(); + } + _ => { + // The quote is not escaped. + // Including idx in the range, so the quote at idx will be printed twice: + // in this call to write_str() and in the next one. + f.write_str(&self.string[start_idx..=idx])?; + start_idx = idx; + } } } _ => { - write!(f, "{ch}")?; peekable_chars.next(); } } previous_char = ch; } + f.write_str(&self.string[start_idx..])?; Ok(()) } } From 74a95fdbd1467b6f7b0f6708d27d5d9e8598eea9 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Wed, 14 May 2025 03:21:23 -0400 Subject: [PATCH 062/130] Add support for `DENY` statements (#1836) --- src/ast/mod.rs | 50 +++++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/mod.rs | 7 +++- src/dialect/mssql.rs | 13 +++++- src/keywords.rs | 1 + src/parser/mod.rs | 84 +++++++++++++++++++++++++++++++++------ tests/sqlparser_common.rs | 33 +++++++++++++++ tests/sqlparser_mssql.rs | 10 +++++ tests/sqlparser_mysql.rs | 1 + 9 files changed, 185 insertions(+), 15 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 79b0e0d5..3791154d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3903,9 +3903,14 @@ pub enum Statement { objects: Option, grantees: Vec, with_grant_option: bool, + as_grantor: Option, granted_by: Option, }, /// ```sql + /// DENY privileges ON object TO grantees + /// ``` + Deny(DenyStatement), + /// ```sql /// REVOKE privileges ON objects FROM grantees /// ``` Revoke { @@ -5580,6 +5585,7 @@ impl fmt::Display for Statement { objects, grantees, with_grant_option, + as_grantor, granted_by, } => { write!(f, "GRANT {privileges} ")?; @@ -5590,11 +5596,15 @@ impl fmt::Display for Statement { if *with_grant_option { write!(f, " WITH GRANT OPTION")?; } + if let Some(grantor) = as_grantor { + write!(f, " AS {grantor}")?; + } if let Some(grantor) = granted_by { write!(f, " GRANTED BY {grantor}")?; } Ok(()) } + Statement::Deny(s) => write!(f, "{s}"), Statement::Revoke { privileges, objects, @@ -6380,6 +6390,9 @@ pub enum Action { }, Delete, EvolveSchema, + Exec { + obj_type: Option, + }, Execute { obj_type: Option, }, @@ -6446,6 +6459,12 @@ impl fmt::Display for Action { Action::DatabaseRole { role } => write!(f, "DATABASE ROLE {role}")?, Action::Delete => f.write_str("DELETE")?, Action::EvolveSchema => f.write_str("EVOLVE SCHEMA")?, + Action::Exec { obj_type } => { + f.write_str("EXEC")?; + if let Some(obj_type) = obj_type { + write!(f, " {obj_type}")? + } + } Action::Execute { obj_type } => { f.write_str("EXECUTE")?; if let Some(obj_type) = obj_type { @@ -6867,6 +6886,37 @@ impl fmt::Display for GrantObjects { } } +/// A `DENY` statement +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/deny-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DenyStatement { + pub privileges: Privileges, + pub objects: GrantObjects, + pub grantees: Vec, + pub granted_by: Option, + pub cascade: Option, +} + +impl fmt::Display for DenyStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DENY {}", self.privileges)?; + write!(f, " ON {}", self.objects)?; + if !self.grantees.is_empty() { + write!(f, " TO {}", display_comma_separated(&self.grantees))?; + } + if let Some(cascade) = &self.cascade { + write!(f, " {cascade}")?; + } + if let Some(granted_by) = &self.granted_by { + write!(f, " AS {granted_by}")?; + } + Ok(()) + } +} + /// SQL assignment `foo = expr` as used in SQLUpdate #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index ff2a61cf..c0935948 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -491,6 +491,7 @@ impl Spanned for Statement { Statement::CreateStage { .. } => Span::empty(), Statement::Assert { .. } => Span::empty(), Statement::Grant { .. } => Span::empty(), + Statement::Deny { .. } => Span::empty(), Statement::Revoke { .. } => Span::empty(), Statement::Deallocate { .. } => Span::empty(), Statement::Execute { .. } => Span::empty(), diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6fbbc7a2..a4c899e6 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -49,7 +49,7 @@ pub use self::postgresql::PostgreSqlDialect; pub use self::redshift::RedshiftSqlDialect; pub use self::snowflake::SnowflakeDialect; pub use self::sqlite::SQLiteDialect; -use crate::ast::{ColumnOption, Expr, Statement}; +use crate::ast::{ColumnOption, Expr, GranteesType, Statement}; pub use crate::keywords; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -910,6 +910,11 @@ pub trait Dialect: Debug + Any { &[] } + /// Returns grantee types that should be treated as identifiers + fn get_reserved_grantees_types(&self) -> &[GranteesType] { + &[] + } + /// Returns true if this dialect supports the `TABLESAMPLE` option /// before the table alias option. For example: /// diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 647e82a2..36bd222b 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -17,8 +17,8 @@ use crate::ast::helpers::attached_token::AttachedToken; use crate::ast::{ - BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, IfStatement, Statement, - TriggerObject, + BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, GranteesType, + IfStatement, Statement, TriggerObject, }; use crate::dialect::Dialect; use crate::keywords::{self, Keyword}; @@ -52,6 +52,10 @@ impl Dialect for MsSqlDialect { || ch == '_' } + fn identifier_quote_style(&self, _identifier: &str) -> Option { + Some('[') + } + /// SQL Server has `CONVERT(type, value)` instead of `CONVERT(value, type)` /// fn convert_type_before_value(&self) -> bool { @@ -119,6 +123,11 @@ impl Dialect for MsSqlDialect { true } + /// See + fn get_reserved_grantees_types(&self) -> &[GranteesType] { + &[GranteesType::Public] + } + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) && !RESERVED_FOR_COLUMN_ALIAS.contains(kw) } diff --git a/src/keywords.rs b/src/keywords.rs index ddb78665..aaa2e167 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -278,6 +278,7 @@ define_keywords!( DELIMITER, DELTA, DENSE_RANK, + DENY, DEREF, DESC, DESCRIBE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d18c7f69..1e022727 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -583,6 +583,10 @@ impl<'a> Parser<'a> { Keyword::SHOW => self.parse_show(), Keyword::USE => self.parse_use(), Keyword::GRANT => self.parse_grant(), + Keyword::DENY => { + self.prev_token(); + self.parse_deny() + } Keyword::REVOKE => self.parse_revoke(), Keyword::START => self.parse_start_transaction(), Keyword::BEGIN => self.parse_begin(), @@ -13381,7 +13385,7 @@ impl<'a> Parser<'a> { /// Parse a GRANT statement. pub fn parse_grant(&mut self) -> Result { - let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::TO)?; let grantees = self.parse_grantees()?; @@ -13389,15 +13393,24 @@ impl<'a> Parser<'a> { let with_grant_option = self.parse_keywords(&[Keyword::WITH, Keyword::GRANT, Keyword::OPTION]); - let granted_by = self - .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) - .then(|| self.parse_identifier().unwrap()); + let as_grantor = if self.parse_keywords(&[Keyword::AS]) { + Some(self.parse_identifier()?) + } else { + None + }; + + let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) { + Some(self.parse_identifier()?) + } else { + None + }; Ok(Statement::Grant { privileges, objects, grantees, with_grant_option, + as_grantor, granted_by, }) } @@ -13406,7 +13419,7 @@ impl<'a> Parser<'a> { let mut values = vec![]; let mut grantee_type = GranteesType::None; loop { - grantee_type = if self.parse_keyword(Keyword::ROLE) { + let new_grantee_type = if self.parse_keyword(Keyword::ROLE) { GranteesType::Role } else if self.parse_keyword(Keyword::USER) { GranteesType::User @@ -13423,9 +13436,19 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::APPLICATION) { GranteesType::Application } else { - grantee_type // keep from previous iteraton, if not specified + grantee_type.clone() // keep from previous iteraton, if not specified }; + if self + .dialect + .get_reserved_grantees_types() + .contains(&new_grantee_type) + { + self.prev_token(); + } else { + grantee_type = new_grantee_type; + } + let grantee = if grantee_type == GranteesType::Public { Grantee { grantee_type: grantee_type.clone(), @@ -13460,7 +13483,7 @@ impl<'a> Parser<'a> { Ok(values) } - pub fn parse_grant_revoke_privileges_objects( + pub fn parse_grant_deny_revoke_privileges_objects( &mut self, ) -> Result<(Privileges, Option), ParserError> { let privileges = if self.parse_keyword(Keyword::ALL) { @@ -13510,7 +13533,6 @@ impl<'a> Parser<'a> { let object_type = self.parse_one_of_keywords(&[ Keyword::SEQUENCE, Keyword::DATABASE, - Keyword::DATABASE, Keyword::SCHEMA, Keyword::TABLE, Keyword::VIEW, @@ -13605,6 +13627,9 @@ impl<'a> Parser<'a> { Ok(Action::Create { obj_type }) } else if self.parse_keyword(Keyword::DELETE) { Ok(Action::Delete) + } else if self.parse_keyword(Keyword::EXEC) { + let obj_type = self.maybe_parse_action_execute_obj_type(); + Ok(Action::Exec { obj_type }) } else if self.parse_keyword(Keyword::EXECUTE) { let obj_type = self.maybe_parse_action_execute_obj_type(); Ok(Action::Execute { obj_type }) @@ -13803,16 +13828,51 @@ impl<'a> Parser<'a> { } } + /// Parse [`Statement::Deny`] + pub fn parse_deny(&mut self) -> Result { + self.expect_keyword(Keyword::DENY)?; + + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; + let objects = match objects { + Some(o) => o, + None => { + return parser_err!( + "DENY statements must specify an object", + self.peek_token().span.start + ) + } + }; + + self.expect_keyword_is(Keyword::TO)?; + let grantees = self.parse_grantees()?; + let cascade = self.parse_cascade_option(); + let granted_by = if self.parse_keywords(&[Keyword::AS]) { + Some(self.parse_identifier()?) + } else { + None + }; + + Ok(Statement::Deny(DenyStatement { + privileges, + objects, + grantees, + cascade, + granted_by, + })) + } + /// Parse a REVOKE statement pub fn parse_revoke(&mut self) -> Result { - let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::FROM)?; let grantees = self.parse_grantees()?; - let granted_by = self - .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) - .then(|| self.parse_identifier().unwrap()); + let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) { + Some(self.parse_identifier()?) + } else { + None + }; let cascade = self.parse_cascade_option(); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7a8b8bda..a07bcc68 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9331,6 +9331,39 @@ fn parse_grant() { verified_stmt("GRANT USAGE ON WAREHOUSE wh1 TO ROLE role1"); verified_stmt("GRANT OWNERSHIP ON INTEGRATION int1 TO ROLE role1"); verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); + verified_stmt("GRANT EXEC ON my_sp TO runner"); + verified_stmt("GRANT UPDATE ON my_table TO updater_role AS dbo"); + + all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) + .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); +} + +#[test] +fn parse_deny() { + let sql = "DENY INSERT, DELETE ON users TO analyst CASCADE AS admin"; + match verified_stmt(sql) { + Statement::Deny(deny) => { + assert_eq!( + Privileges::Actions(vec![Action::Insert { columns: None }, Action::Delete]), + deny.privileges + ); + assert_eq!( + &GrantObjects::Tables(vec![ObjectName::from(vec![Ident::new("users")])]), + &deny.objects + ); + assert_eq_vec(&["analyst"], &deny.grantees); + assert_eq!(Some(CascadeOption::Cascade), deny.cascade); + assert_eq!(Some(Ident::from("admin")), deny.granted_by); + } + _ => unreachable!(), + } + + verified_stmt("DENY SELECT, INSERT, UPDATE, DELETE ON db1.sc1 TO role1, role2"); + verified_stmt("DENY ALL ON db1.sc1 TO role1"); + verified_stmt("DENY EXEC ON my_sp TO runner"); + + all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) + .verified_stmt("DENY SELECT ON [my_table] TO [public]"); } #[test] diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 1c0a00b1..7b3769ec 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2340,3 +2340,13 @@ fn parse_print() { let _ = ms().verified_stmt("PRINT N'Hello, ⛄️!'"); let _ = ms().verified_stmt("PRINT @my_variable"); } + +#[test] +fn parse_mssql_grant() { + ms().verified_stmt("GRANT SELECT ON my_table TO public, db_admin"); +} + +#[test] +fn parse_mssql_deny() { + ms().verified_stmt("DENY SELECT ON my_table TO public, db_admin"); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 27c60b05..dd279894 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3538,6 +3538,7 @@ fn parse_grant() { objects, grantees, with_grant_option, + as_grantor: _, granted_by, } = stmt { From c6e897dc12428891fec1a1ec5421c5e43e7080b2 Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Wed, 14 May 2025 08:40:44 +0100 Subject: [PATCH 063/130] Postgresql: Add `REPLICA IDENTITY` operation for `ALTER TABLE` (#1844) --- src/ast/ddl.rs | 33 +++++++++++++++++++++++++++++++++ src/ast/mod.rs | 2 +- src/ast/spans.rs | 1 + src/parser/mod.rs | 19 ++++++++++++++++++- tests/sqlparser_postgres.rs | 27 +++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a457a065..27089713 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -39,6 +39,29 @@ use crate::ast::{ use crate::keywords::Keyword; use crate::tokenizer::Token; +/// ALTER TABLE operation REPLICA IDENTITY values +/// See [Postgres ALTER TABLE docs](https://www.postgresql.org/docs/current/sql-altertable.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ReplicaIdentity { + None, + Full, + Default, + Index(Ident), +} + +impl fmt::Display for ReplicaIdentity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ReplicaIdentity::None => f.write_str("NONE"), + ReplicaIdentity::Full => f.write_str("FULL"), + ReplicaIdentity::Default => f.write_str("DEFAULT"), + ReplicaIdentity::Index(idx) => write!(f, "USING INDEX {}", idx), + } + } +} + /// An `ALTER TABLE` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -208,6 +231,13 @@ pub enum AlterTableOperation { old_partitions: Vec, new_partitions: Vec, }, + /// REPLICA IDENTITY { DEFAULT | USING INDEX index_name | FULL | NOTHING } + /// + /// Note: this is a PostgreSQL-specific operation. + /// Please refer to [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-altertable.html) + ReplicaIdentity { + identity: ReplicaIdentity, + }, /// Add Partitions AddPartitions { if_not_exists: bool, @@ -729,6 +759,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::Lock { equals, lock } => { write!(f, "LOCK {}{}", if *equals { "= " } else { "" }, lock) } + AlterTableOperation::ReplicaIdentity { identity } => { + write!(f, "REPLICA IDENTITY {identity}") + } } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3791154d..d1d706e4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -65,7 +65,7 @@ pub use self::ddl::{ DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, - ProcedureParam, ReferentialAction, TableConstraint, TagsColumnOption, + ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index c0935948..cb1c48ca 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1175,6 +1175,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::Algorithm { .. } => Span::empty(), AlterTableOperation::AutoIncrement { value, .. } => value.span(), AlterTableOperation::Lock { .. } => Span::empty(), + AlterTableOperation::ReplicaIdentity { .. } => Span::empty(), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1e022727..4665bb76 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8774,6 +8774,23 @@ impl<'a> Parser<'a> { let equals = self.consume_token(&Token::Eq); let value = self.parse_number_value()?; AlterTableOperation::AutoIncrement { equals, value } + } else if self.parse_keywords(&[Keyword::REPLICA, Keyword::IDENTITY]) { + let identity = if self.parse_keyword(Keyword::NONE) { + ReplicaIdentity::None + } else if self.parse_keyword(Keyword::FULL) { + ReplicaIdentity::Full + } else if self.parse_keyword(Keyword::DEFAULT) { + ReplicaIdentity::Default + } else if self.parse_keywords(&[Keyword::USING, Keyword::INDEX]) { + ReplicaIdentity::Index(self.parse_identifier()?) + } else { + return self.expected( + "NONE, FULL, DEFAULT, or USING INDEX index_name after REPLICA IDENTITY", + self.peek_token(), + ); + }; + + AlterTableOperation::ReplicaIdentity { identity } } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; @@ -8783,7 +8800,7 @@ impl<'a> Parser<'a> { } } else { return self.expected( - "ADD, RENAME, PARTITION, SWAP, DROP, or SET TBLPROPERTIES after ALTER TABLE", + "ADD, RENAME, PARTITION, SWAP, DROP, REPLICA IDENTITY, or SET TBLPROPERTIES after ALTER TABLE", self.peek_token(), ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 859eca45..9e71883c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5959,3 +5959,30 @@ fn parse_varbit_datatype() { _ => unreachable!(), } } + +#[test] +fn parse_alter_table_replica_identity() { + match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY FULL") { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::ReplicaIdentity { + identity: ReplicaIdentity::Full + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY USING INDEX foo_idx") { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::ReplicaIdentity { + identity: ReplicaIdentity::Index("foo_idx".into()) + }] + ); + } + _ => unreachable!(), + } +} From 3c5995006073be1f9d9d445f578dd206ee889d9e Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Thu, 15 May 2025 17:40:21 +0300 Subject: [PATCH 064/130] Add support for INCLUDE/EXCLUDE NULLS for UNPIVOT (#1849) --- src/ast/mod.rs | 20 +++++++ src/ast/query.rs | 11 +++- src/ast/spans.rs | 1 + src/parser/mod.rs | 10 ++++ tests/sqlparser_common.rs | 113 ++++++++++++++++++++++++-------------- 5 files changed, 111 insertions(+), 44 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d1d706e4..1ad1b6da 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9633,6 +9633,26 @@ impl fmt::Display for OpenStatement { } } +/// Specifies Include / Exclude NULL within UNPIVOT command. +/// For example +/// `UNPIVOT (column1 FOR new_column IN (col3, col4, col5, col6))` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum NullInclusion { + IncludeNulls, + ExcludeNulls, +} + +impl fmt::Display for NullInclusion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NullInclusion::IncludeNulls => write!(f, "INCLUDE NULLS"), + NullInclusion::ExcludeNulls => write!(f, "EXCLUDE NULLS"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/query.rs b/src/ast/query.rs index 33168695..adb8516f 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1336,7 +1336,7 @@ pub enum TableFactor { /// /// Syntax: /// ```sql - /// table UNPIVOT(value FOR name IN (column1, [ column2, ... ])) [ alias ] + /// table UNPIVOT [ { INCLUDE | EXCLUDE } NULLS ] (value FOR name IN (column1, [ column2, ... ])) [ alias ] /// ``` /// /// See . @@ -1345,6 +1345,7 @@ pub enum TableFactor { value: Ident, name: Ident, columns: Vec, + null_inclusion: Option, alias: Option, }, /// A `MATCH_RECOGNIZE` operation on a table. @@ -2015,15 +2016,19 @@ impl fmt::Display for TableFactor { } TableFactor::Unpivot { table, + null_inclusion, value, name, columns, alias, } => { + write!(f, "{table} UNPIVOT")?; + if let Some(null_inclusion) = null_inclusion { + write!(f, " {null_inclusion} ")?; + } write!( f, - "{} UNPIVOT({} FOR {} IN ({}))", - table, + "({} FOR {} IN ({}))", value, name, display_comma_separated(columns) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index cb1c48ca..bffd1172 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1944,6 +1944,7 @@ impl Spanned for TableFactor { TableFactor::Unpivot { table, value, + null_inclusion: _, name, columns, alias, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4665bb76..83899613 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13368,6 +13368,15 @@ impl<'a> Parser<'a> { &mut self, table: TableFactor, ) -> Result { + let null_inclusion = if self.parse_keyword(Keyword::INCLUDE) { + self.expect_keyword_is(Keyword::NULLS)?; + Some(NullInclusion::IncludeNulls) + } else if self.parse_keyword(Keyword::EXCLUDE) { + self.expect_keyword_is(Keyword::NULLS)?; + Some(NullInclusion::ExcludeNulls) + } else { + None + }; self.expect_token(&Token::LParen)?; let value = self.parse_identifier()?; self.expect_keyword_is(Keyword::FOR)?; @@ -13379,6 +13388,7 @@ impl<'a> Parser<'a> { Ok(TableFactor::Unpivot { table: Box::new(table), value, + null_inclusion, name, columns, alias, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a07bcc68..8e3bc002 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10746,49 +10746,47 @@ fn parse_unpivot_table() { "SELECT * FROM sales AS s ", "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" ); - - pretty_assertions::assert_eq!( - verified_only_select(sql).from[0].relation, - Unpivot { - table: Box::new(TableFactor::Table { - name: ObjectName::from(vec![Ident::new("sales")]), - alias: Some(TableAlias { - name: Ident::new("s"), - columns: vec![] - }), - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - sample: None, - index_hints: vec![], - }), - value: Ident { - value: "quantity".to_string(), - quote_style: None, - span: Span::empty() - }, - - name: Ident { - value: "quarter".to_string(), - quote_style: None, - span: Span::empty() - }, - columns: ["Q1", "Q2", "Q3", "Q4"] - .into_iter() - .map(Ident::new) - .collect(), + let base_unpivot = Unpivot { + table: Box::new(TableFactor::Table { + name: ObjectName::from(vec![Ident::new("sales")]), alias: Some(TableAlias { - name: Ident::new("u"), - columns: ["product", "quarter", "quantity"] - .into_iter() - .map(TableAliasColumnDef::from_name) - .collect(), + name: Ident::new("s"), + columns: vec![], }), - } - ); + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }), + null_inclusion: None, + value: Ident { + value: "quantity".to_string(), + quote_style: None, + span: Span::empty(), + }, + + name: Ident { + value: "quarter".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: ["Q1", "Q2", "Q3", "Q4"] + .into_iter() + .map(Ident::new) + .collect(), + alias: Some(TableAlias { + name: Ident::new("u"), + columns: ["product", "quarter", "quantity"] + .into_iter() + .map(TableAliasColumnDef::from_name) + .collect(), + }), + }; + pretty_assertions::assert_eq!(verified_only_select(sql).from[0].relation, base_unpivot); assert_eq!(verified_stmt(sql).to_string(), sql); let sql_without_aliases = concat!( @@ -10808,6 +10806,38 @@ fn parse_unpivot_table() { verified_stmt(sql_without_aliases).to_string(), sql_without_aliases ); + + let sql_unpivot_exclude_nulls = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT EXCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + ); + + if let Unpivot { null_inclusion, .. } = + &verified_only_select(sql_unpivot_exclude_nulls).from[0].relation + { + assert_eq!(*null_inclusion, Some(NullInclusion::ExcludeNulls)); + } + + assert_eq!( + verified_stmt(sql_unpivot_exclude_nulls).to_string(), + sql_unpivot_exclude_nulls + ); + + let sql_unpivot_include_nulls = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + ); + + if let Unpivot { null_inclusion, .. } = + &verified_only_select(sql_unpivot_include_nulls).from[0].relation + { + assert_eq!(*null_inclusion, Some(NullInclusion::IncludeNulls)); + } + + assert_eq!( + verified_stmt(sql_unpivot_include_nulls).to_string(), + sql_unpivot_include_nulls + ); } #[test] @@ -10904,6 +10934,7 @@ fn parse_pivot_unpivot_table() { sample: None, index_hints: vec![], }), + null_inclusion: None, value: Ident { value: "population".to_string(), quote_style: None, From ae587dcbec9f6d2d3cde415ead7e8e376167355c Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Thu, 15 May 2025 16:43:16 +0200 Subject: [PATCH 065/130] pretty print improvements (#1851) --- README.md | 4 + src/ast/dml.rs | 57 ++++++---- src/ast/mod.rs | 36 ++++--- src/ast/query.rs | 9 +- src/display_utils.rs | 55 ++++------ tests/pretty_print.rs | 242 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 334 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 6acfbcef..666be17c 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,14 @@ keywords, the following should hold true for all SQL: ```rust // Parse SQL +let sql = "SELECT 'hello'"; let ast = Parser::parse_sql(&GenericDialect, sql).unwrap(); // The original SQL text can be generated from the AST assert_eq!(ast[0].to_string(), sql); + +// The SQL can also be pretty-printed with newlines and indentation +assert_eq!(format!("{:#}", ast[0]), "SELECT\n 'hello'"); ``` There are still some cases in this crate where different SQL with seemingly diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 7ed17be9..e4081a63 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -29,6 +29,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; + pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ @@ -579,28 +581,32 @@ impl Display for Insert { )?; } if !self.columns.is_empty() { - write!(f, "({}) ", display_comma_separated(&self.columns))?; + write!(f, "({})", display_comma_separated(&self.columns))?; + SpaceOrNewline.fmt(f)?; } if let Some(ref parts) = self.partitioned { if !parts.is_empty() { - write!(f, "PARTITION ({}) ", display_comma_separated(parts))?; + write!(f, "PARTITION ({})", display_comma_separated(parts))?; + SpaceOrNewline.fmt(f)?; } } if !self.after_columns.is_empty() { - write!(f, "({}) ", display_comma_separated(&self.after_columns))?; + write!(f, "({})", display_comma_separated(&self.after_columns))?; + SpaceOrNewline.fmt(f)?; } if let Some(settings) = &self.settings { - write!(f, "SETTINGS {} ", display_comma_separated(settings))?; + write!(f, "SETTINGS {}", display_comma_separated(settings))?; + SpaceOrNewline.fmt(f)?; } if let Some(source) = &self.source { - write!(f, "{source}")?; + source.fmt(f)?; } else if !self.assignments.is_empty() { - write!(f, "SET ")?; - write!(f, "{}", display_comma_separated(&self.assignments))?; + write!(f, "SET")?; + indented_list(f, &self.assignments)?; } else if let Some(format_clause) = &self.format_clause { - write!(f, "{format_clause}")?; + format_clause.fmt(f)?; } else if self.columns.is_empty() { write!(f, "DEFAULT VALUES")?; } @@ -620,7 +626,9 @@ impl Display for Insert { } if let Some(returning) = &self.returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; } Ok(()) } @@ -649,32 +657,45 @@ pub struct Delete { impl Display for Delete { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "DELETE ")?; + f.write_str("DELETE")?; if !self.tables.is_empty() { - write!(f, "{} ", display_comma_separated(&self.tables))?; + indented_list(f, &self.tables)?; } match &self.from { FromTable::WithFromKeyword(from) => { - write!(f, "FROM {}", display_comma_separated(from))?; + f.write_str(" FROM")?; + indented_list(f, from)?; } FromTable::WithoutKeyword(from) => { - write!(f, "{}", display_comma_separated(from))?; + indented_list(f, from)?; } } if let Some(using) = &self.using { - write!(f, " USING {}", display_comma_separated(using))?; + SpaceOrNewline.fmt(f)?; + f.write_str("USING")?; + indented_list(f, using)?; } if let Some(selection) = &self.selection { - write!(f, " WHERE {selection}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; } if let Some(returning) = &self.returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; } if !self.order_by.is_empty() { - write!(f, " ORDER BY {}", display_comma_separated(&self.order_by))?; + SpaceOrNewline.fmt(f)?; + f.write_str("ORDER BY")?; + indented_list(f, &self.order_by)?; } if let Some(limit) = &self.limit { - write!(f, " LIMIT {limit}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("LIMIT")?; + SpaceOrNewline.fmt(f)?; + Indent(limit).fmt(f)?; } Ok(()) } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1ad1b6da..d711a106 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -41,7 +41,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::{ - display_utils::SpaceOrNewline, + display_utils::{indented_list, SpaceOrNewline}, tokenizer::{Span, Token}, }; use crate::{ @@ -4548,7 +4548,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Insert(insert) => write!(f, "{insert}"), + Statement::Insert(insert) => insert.fmt(f), Statement::Install { extension_name: name, } => write!(f, "INSTALL {name}"), @@ -4611,30 +4611,42 @@ impl fmt::Display for Statement { returning, or, } => { - write!(f, "UPDATE ")?; + f.write_str("UPDATE ")?; if let Some(or) = or { - write!(f, "{or} ")?; + or.fmt(f)?; + f.write_str(" ")?; } - write!(f, "{table}")?; + table.fmt(f)?; if let Some(UpdateTableFromKind::BeforeSet(from)) = from { - write!(f, " FROM {}", display_comma_separated(from))?; + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; } if !assignments.is_empty() { - write!(f, " SET {}", display_comma_separated(assignments))?; + SpaceOrNewline.fmt(f)?; + f.write_str("SET")?; + indented_list(f, assignments)?; } if let Some(UpdateTableFromKind::AfterSet(from)) = from { - write!(f, " FROM {}", display_comma_separated(from))?; + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; } if let Some(selection) = selection { - write!(f, " WHERE {selection}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; } if let Some(returning) = returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; } Ok(()) } - Statement::Delete(delete) => write!(f, "{delete}"), - Statement::Open(open) => write!(f, "{open}"), + Statement::Delete(delete) => delete.fmt(f), + Statement::Open(open) => open.fmt(f), Statement::Close { cursor } => { write!(f, "CLOSE {cursor}")?; diff --git a/src/ast/query.rs b/src/ast/query.rs index adb8516f..5b784b19 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2888,13 +2888,14 @@ pub struct Values { impl fmt::Display for Values { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "VALUES ")?; + f.write_str("VALUES")?; let prefix = if self.explicit_row { "ROW" } else { "" }; let mut delim = ""; for row in &self.rows { - write!(f, "{delim}")?; - delim = ", "; - write!(f, "{prefix}({})", display_comma_separated(row))?; + f.write_str(delim)?; + delim = ","; + SpaceOrNewline.fmt(f)?; + Indent(format_args!("{prefix}({})", display_comma_separated(row))).fmt(f)?; } Ok(()) } diff --git a/src/display_utils.rs b/src/display_utils.rs index e7e1272f..849aea94 100644 --- a/src/display_utils.rs +++ b/src/display_utils.rs @@ -31,13 +31,10 @@ where T: Write, { fn write_str(&mut self, s: &str) -> fmt::Result { - let mut first = true; - for line in s.split('\n') { - if !first { - write!(self.0, "\n{INDENT}")?; - } - self.0.write_str(line)?; - first = false; + self.0.write_str(s)?; + // Our NewLine and SpaceOrNewline utils always print individual newlines as a single-character string. + if s == "\n" { + self.0.write_str(INDENT)?; } Ok(()) } @@ -71,7 +68,7 @@ impl Display for SpaceOrNewline { /// A value that displays a comma-separated list of values. /// When pretty-printed (using {:#}), it displays each value on a new line. -pub struct DisplayCommaSeparated<'a, T: fmt::Display>(&'a [T]); +pub(crate) struct DisplayCommaSeparated<'a, T: fmt::Display>(&'a [T]); impl fmt::Display for DisplayCommaSeparated<'_, T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -89,45 +86,33 @@ impl fmt::Display for DisplayCommaSeparated<'_, T> { } /// Displays a whitespace, followed by a comma-separated list that is indented when pretty-printed. -pub(crate) fn indented_list(f: &mut fmt::Formatter, slice: &[T]) -> fmt::Result { +pub(crate) fn indented_list(f: &mut fmt::Formatter, items: &[T]) -> fmt::Result { SpaceOrNewline.fmt(f)?; - Indent(DisplayCommaSeparated(slice)).fmt(f) + Indent(DisplayCommaSeparated(items)).fmt(f) } #[cfg(test)] mod tests { use super::*; - struct DisplayCharByChar(T); - - impl Display for DisplayCharByChar { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for c in self.0.to_string().chars() { - write!(f, "{}", c)?; - } - Ok(()) - } - } - #[test] fn test_indent() { - let original = "line 1\nline 2"; - let indent = Indent(original); + struct TwoLines; + + impl Display for TwoLines { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("line 1")?; + SpaceOrNewline.fmt(f)?; + f.write_str("line 2") + } + } + + let indent = Indent(TwoLines); assert_eq!( indent.to_string(), - original, + TwoLines.to_string(), "Only the alternate form should be indented" ); - let expected = " line 1\n line 2"; - assert_eq!(format!("{:#}", indent), expected); - let display_char_by_char = DisplayCharByChar(original); - assert_eq!(format!("{:#}", Indent(display_char_by_char)), expected); - } - - #[test] - fn test_space_or_newline() { - let space_or_newline = SpaceOrNewline; - assert_eq!(format!("{}", space_or_newline), " "); - assert_eq!(format!("{:#}", space_or_newline), "\n"); + assert_eq!(format!("{:#}", indent), " line 1\n line 2"); } } diff --git a/tests/pretty_print.rs b/tests/pretty_print.rs index 1eb8ca41..b4adbe35 100644 --- a/tests/pretty_print.rs +++ b/tests/pretty_print.rs @@ -155,3 +155,245 @@ FROM "#.trim() ); } + +#[test] +fn test_pretty_print_multiline_string() { + assert_eq!( + prettify("SELECT 'multiline\nstring' AS str"), + r#" +SELECT + 'multiline +string' AS str +"# + .trim(), + "A literal string with a newline should be kept as is. The contents of the string should not be indented." + ); +} + +#[test] +fn test_pretty_print_insert_values() { + assert_eq!( + prettify("INSERT INTO my_table (a, b, c) VALUES (1, 2, 3), (4, 5, 6)"), + r#" +INSERT INTO my_table (a, b, c) +VALUES + (1, 2, 3), + (4, 5, 6) +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_insert_select() { + assert_eq!( + prettify("INSERT INTO my_table (a, b) SELECT x, y FROM source_table RETURNING a AS id"), + r#" +INSERT INTO my_table (a, b) +SELECT + x, + y +FROM + source_table +RETURNING + a AS id +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_update() { + assert_eq!( + prettify("UPDATE my_table SET a = 1, b = 2 WHERE x > 0 RETURNING id, name"), + r#" +UPDATE my_table +SET + a = 1, + b = 2 +WHERE + x > 0 +RETURNING + id, + name +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_delete() { + assert_eq!( + prettify("DELETE FROM my_table WHERE x > 0 RETURNING id, name"), + r#" +DELETE FROM + my_table +WHERE + x > 0 +RETURNING + id, + name +"# + .trim() + ); + + assert_eq!( + prettify("DELETE table1, table2"), + r#" +DELETE + table1, + table2 +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_create_table() { + assert_eq!( + prettify("CREATE TABLE my_table (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, CONSTRAINT fk_other FOREIGN KEY (id) REFERENCES other_table(id))"), + r#" +CREATE TABLE my_table ( + id INT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + CONSTRAINT fk_other FOREIGN KEY (id) REFERENCES other_table(id) +) +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_create_view() { + assert_eq!( + prettify("CREATE VIEW my_view AS SELECT a, b FROM my_table WHERE x > 0"), + r#" +CREATE VIEW my_view AS +SELECT + a, + b +FROM + my_table +WHERE + x > 0 +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_create_function() { + assert_eq!( + prettify("CREATE FUNCTION my_func() RETURNS INT BEGIN SELECT COUNT(*) INTO @count FROM my_table; RETURN @count; END"), + r#" +CREATE FUNCTION my_func() RETURNS INT +BEGIN + SELECT COUNT(*) INTO @count FROM my_table; + RETURN @count; +END +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_json_table() { + assert_eq!( + prettify("SELECT * FROM JSON_TABLE(@json, '$[*]' COLUMNS (id INT PATH '$.id', name VARCHAR(255) PATH '$.name')) AS jt"), + r#" +SELECT + * +FROM + JSON_TABLE( + @json, + '$[*]' COLUMNS ( + id INT PATH '$.id', + name VARCHAR(255) PATH '$.name' + ) + ) AS jt +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_transaction_blocks() { + assert_eq!( + prettify("BEGIN; UPDATE my_table SET x = 1; COMMIT;"), + r#" +BEGIN; +UPDATE my_table SET x = 1; +COMMIT; +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_control_flow() { + assert_eq!( + prettify("IF x > 0 THEN SELECT 'positive'; ELSE SELECT 'negative'; END IF;"), + r#" +IF x > 0 THEN + SELECT 'positive'; +ELSE + SELECT 'negative'; +END IF; +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_merge() { + assert_eq!( + prettify("MERGE INTO target_table t USING source_table s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.value = s.value WHEN NOT MATCHED THEN INSERT (id, value) VALUES (s.id, s.value)"), + r#" +MERGE INTO target_table t +USING source_table s ON t.id = s.id +WHEN MATCHED THEN + UPDATE SET t.value = s.value +WHEN NOT MATCHED THEN + INSERT (id, value) VALUES (s.id, s.value) +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_create_index() { + assert_eq!( + prettify("CREATE INDEX idx_name ON my_table (column1, column2)"), + r#" +CREATE INDEX idx_name +ON my_table (column1, column2) +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_explain() { + assert_eq!( + prettify("EXPLAIN ANALYZE SELECT * FROM my_table WHERE x > 0"), + r#" +EXPLAIN ANALYZE +SELECT + * +FROM + my_table +WHERE + x > 0 +"# + .trim() + ); +} From e7bf186e44a35628af2c2581ad7e82ea8e8df2d3 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 19 May 2025 13:09:00 +0200 Subject: [PATCH 066/130] fix new rust 1.87 cargo clippy warnings (#1856) --- src/ast/visitor.rs | 13 ++++++++----- src/lib.rs | 4 ++++ src/parser/mod.rs | 5 +---- tests/sqlparser_common.rs | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 50985a3e..ab4f73aa 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -741,7 +741,7 @@ mod tests { } } - fn do_visit(sql: &str, visitor: &mut V) -> Statement { + fn do_visit>(sql: &str, visitor: &mut V) -> Statement { let dialect = GenericDialect {}; let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let s = Parser::new(&dialect) @@ -749,7 +749,8 @@ mod tests { .parse_statement() .unwrap(); - s.visit(visitor); + let flow = s.visit(visitor); + assert_eq!(flow, ControlFlow::Continue(())); s } @@ -938,7 +939,8 @@ mod tests { .unwrap(); let mut visitor = QuickVisitor {}; - s.visit(&mut visitor); + let flow = s.visit(&mut visitor); + assert_eq!(flow, ControlFlow::Continue(())); } } @@ -969,7 +971,7 @@ mod visit_mut_tests { } } - fn do_visit_mut(sql: &str, visitor: &mut V) -> Statement { + fn do_visit_mut>(sql: &str, visitor: &mut V) -> Statement { let dialect = GenericDialect {}; let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let mut s = Parser::new(&dialect) @@ -977,7 +979,8 @@ mod visit_mut_tests { .parse_statement() .unwrap(); - s.visit(visitor); + let flow = s.visit(visitor); + assert_eq!(flow, ControlFlow::Continue(())); s } diff --git a/src/lib.rs b/src/lib.rs index c81ab500..dbfd1791 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,6 +149,10 @@ #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::upper_case_acronyms)] +// Permit large enum variants to keep a unified, expressive AST. +// Splitting complex nodes (expressions, statements, types) into separate types +// would bloat the API and hide intent. Extra memory is a worthwhile tradeoff. +#![allow(clippy::large_enum_variant)] // Allow proc-macros to find this crate extern crate self as sqlparser; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 83899613..992d19c4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2558,10 +2558,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let mut trim_where = None; if let Token::Word(word) = self.peek_token().token { - if [Keyword::BOTH, Keyword::LEADING, Keyword::TRAILING] - .iter() - .any(|d| word.keyword == *d) - { + if [Keyword::BOTH, Keyword::LEADING, Keyword::TRAILING].contains(&word.keyword) { trim_where = Some(self.parse_trim_where()?); } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8e3bc002..86c473d7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14346,7 +14346,7 @@ fn test_visit_order() { let sql = "SELECT CASE a WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE 5 END"; let stmt = verified_stmt(sql); let mut visited = vec![]; - sqlparser::ast::visit_expressions(&stmt, |expr| { + let _ = sqlparser::ast::visit_expressions(&stmt, |expr| { visited.push(expr.to_string()); core::ops::ControlFlow::<()>::Continue(()) }); From 525ed81fdeffb3d765be99fc3cc4fd31060e0df1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 06:21:41 +0200 Subject: [PATCH 067/130] Update criterion requirement from 0.5 to 0.6 in /sqlparser_bench (#1857) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ifeanyi --- sqlparser_bench/Cargo.toml | 2 +- sqlparser_bench/benches/sqlparser_bench.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlparser_bench/Cargo.toml b/sqlparser_bench/Cargo.toml index 2c1f0ae4..01c59be7 100644 --- a/sqlparser_bench/Cargo.toml +++ b/sqlparser_bench/Cargo.toml @@ -26,7 +26,7 @@ edition = "2018" sqlparser = { path = "../" } [dev-dependencies] -criterion = "0.5" +criterion = "0.6" [[bench]] name = "sqlparser_bench" diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs index a7768cbc..24c59c07 100644 --- a/sqlparser_bench/benches/sqlparser_bench.rs +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -68,7 +68,7 @@ fn basic_queries(c: &mut Criterion) { }; group.bench_function("parse_large_statement", |b| { - b.iter(|| Parser::parse_sql(&dialect, criterion::black_box(large_statement.as_str()))); + b.iter(|| Parser::parse_sql(&dialect, std::hint::black_box(large_statement.as_str()))); }); let large_statement = Parser::parse_sql(&dialect, large_statement.as_str()) From a496f78803d1ceda4a09f5b5d952636f19175f16 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 20 May 2025 18:34:48 +0200 Subject: [PATCH 068/130] pretty-print CREATE TABLE statements (#1854) --- src/ast/dml.rs | 15 ++++++++++----- src/display_utils.rs | 2 +- tests/pretty_print.rs | 1 - 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index e4081a63..da82a4ed 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -29,7 +29,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; +use crate::display_utils::{indented_list, DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; pub use super::ddl::{ColumnDef, TableConstraint}; @@ -267,14 +267,19 @@ impl Display for CreateTable { write!(f, " ON CLUSTER {}", on_cluster)?; } if !self.columns.is_empty() || !self.constraints.is_empty() { - write!(f, " ({}", display_comma_separated(&self.columns))?; + f.write_str(" (")?; + NewLine.fmt(f)?; + Indent(DisplayCommaSeparated(&self.columns)).fmt(f)?; if !self.columns.is_empty() && !self.constraints.is_empty() { - write!(f, ", ")?; + f.write_str(",")?; + SpaceOrNewline.fmt(f)?; } - write!(f, "{})", display_comma_separated(&self.constraints))?; + Indent(DisplayCommaSeparated(&self.constraints)).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; } else if self.query.is_none() && self.like.is_none() && self.clone.is_none() { // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens - write!(f, " ()")?; + f.write_str(" ()")?; } // Hive table comment should be after column definitions, please refer to: diff --git a/src/display_utils.rs b/src/display_utils.rs index 849aea94..e594a34e 100644 --- a/src/display_utils.rs +++ b/src/display_utils.rs @@ -68,7 +68,7 @@ impl Display for SpaceOrNewline { /// A value that displays a comma-separated list of values. /// When pretty-printed (using {:#}), it displays each value on a new line. -pub(crate) struct DisplayCommaSeparated<'a, T: fmt::Display>(&'a [T]); +pub(crate) struct DisplayCommaSeparated<'a, T: fmt::Display>(pub(crate) &'a [T]); impl fmt::Display for DisplayCommaSeparated<'_, T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { diff --git a/tests/pretty_print.rs b/tests/pretty_print.rs index b4adbe35..d6794218 100644 --- a/tests/pretty_print.rs +++ b/tests/pretty_print.rs @@ -249,7 +249,6 @@ DELETE } #[test] -#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] fn test_pretty_print_create_table() { assert_eq!( prettify("CREATE TABLE my_table (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, CONSTRAINT fk_other FOREIGN KEY (id) REFERENCES other_table(id))"), From 3f4d5f96ee2622a0a2ad3fedf56dfd93baeb9a03 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Wed, 21 May 2025 05:44:33 +0200 Subject: [PATCH 069/130] pretty-print CREATE VIEW statements (#1855) --- src/ast/mod.rs | 4 +++- tests/pretty_print.rs | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d711a106..e18251ea 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4858,7 +4858,9 @@ impl fmt::Display for Statement { if matches!(options, CreateTableOptions::Options(_)) { write!(f, " {options}")?; } - write!(f, " AS {query}")?; + f.write_str(" AS")?; + SpaceOrNewline.fmt(f)?; + query.fmt(f)?; if *with_no_schema_binding { write!(f, " WITH NO SCHEMA BINDING")?; } diff --git a/tests/pretty_print.rs b/tests/pretty_print.rs index d6794218..e1d35eb0 100644 --- a/tests/pretty_print.rs +++ b/tests/pretty_print.rs @@ -264,7 +264,6 @@ CREATE TABLE my_table ( } #[test] -#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] fn test_pretty_print_create_view() { assert_eq!( prettify("CREATE VIEW my_view AS SELECT a, b FROM my_table WHERE x > 0"), From 05d7ffb1d5ef6e4c4852a200e0aa08fec224aa3c Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Wed, 21 May 2025 05:49:28 +0200 Subject: [PATCH 070/130] Handle optional datatypes properly in `CREATE FUNCTION` statements (#1826) Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 19 +++- tests/sqlparser_postgres.rs | 211 ++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 992d19c4..f6a45ada 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5273,12 +5273,21 @@ impl<'a> Parser<'a> { // parse: [ argname ] argtype let mut name = None; let mut data_type = self.parse_data_type()?; - if let DataType::Custom(n, _) = &data_type { - // the first token is actually a name - match n.0[0].clone() { - ObjectNamePart::Identifier(ident) => name = Some(ident), + + // To check whether the first token is a name or a type, we need to + // peek the next token, which if it is another type keyword, then the + // first token is a name and not a type in itself. + let data_type_idx = self.get_current_index(); + if let Some(next_data_type) = self.maybe_parse(|parser| parser.parse_data_type())? { + let token = self.token_at(data_type_idx); + + // We ensure that the token is a `Word` token, and not other special tokens. + if !matches!(token.token, Token::Word(_)) { + return self.expected("a name or type", token.clone()); } - data_type = self.parse_data_type()?; + + name = Some(Ident::new(token.to_string())); + data_type = next_data_type; } let default_expr = if self.parse_keyword(Keyword::DEFAULT) || self.consume_token(&Token::Eq) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9e71883c..682c0d6c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -21,6 +21,7 @@ #[macro_use] mod test_utils; + use helpers::attached_token::AttachedToken; use sqlparser::tokenizer::Span; use test_utils::*; @@ -4105,6 +4106,216 @@ fn parse_update_in_with_subquery() { pg_and_generic().verified_stmt(r#"WITH "result" AS (UPDATE "Hero" SET "name" = 'Captain America', "number_of_movies" = "number_of_movies" + 1 WHERE "secret_identity" = 'Sam Wilson' RETURNING "id", "name", "secret_identity", "number_of_movies") SELECT * FROM "result""#); } +#[test] +fn parser_create_function_with_args() { + let sql1 = r#"CREATE OR REPLACE FUNCTION check_strings_different(str1 VARCHAR, str2 VARCHAR) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +BEGIN + IF str1 <> str2 THEN + RETURN TRUE; + ELSE + RETURN FALSE; + END IF; +END; +$$"#; + + assert_eq!( + pg_and_generic().verified_stmt(sql1), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("check_strings_different")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "str1", + DataType::Varchar(None), + ), + OperateFunctionArg::with_name( + "str2", + DataType::Varchar(None), + ), + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF str1 <> str2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let sql2 = r#"CREATE OR REPLACE FUNCTION check_not_zero(int1 INT) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +BEGIN + IF int1 <> 0 THEN + RETURN TRUE; + ELSE + RETURN FALSE; + END IF; +END; +$$"#; + assert_eq!( + pg_and_generic().verified_stmt(sql2), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("check_not_zero")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "int1", + DataType::Int(None) + ) + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> 0 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let sql3 = r#"CREATE OR REPLACE FUNCTION check_values_different(a INT, b INT) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +BEGIN + IF a <> b THEN + RETURN TRUE; + ELSE + RETURN FALSE; + END IF; +END; +$$"#; + assert_eq!( + pg_and_generic().verified_stmt(sql3), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("check_values_different")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "a", + DataType::Int(None) + ), + OperateFunctionArg::with_name( + "b", + DataType::Int(None) + ), + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF a <> b THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let sql4 = r#"CREATE OR REPLACE FUNCTION check_values_different(int1 INT, int2 INT) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +BEGIN + IF int1 <> int2 THEN + RETURN TRUE; + ELSE + RETURN FALSE; + END IF; +END; +$$"#; + assert_eq!( + pg_and_generic().verified_stmt(sql4), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("check_values_different")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "int1", + DataType::Int(None) + ), + OperateFunctionArg::with_name( + "int2", + DataType::Int(None) + ), + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> int2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let sql5 = r#"CREATE OR REPLACE FUNCTION foo(a TIMESTAMP WITH TIME ZONE, b VARCHAR) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ + BEGIN + RETURN TRUE; + END; + $$"#; + assert_eq!( + pg_and_generic().verified_stmt(sql5), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("foo")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "a", + DataType::Timestamp(None, TimezoneInfo::WithTimeZone) + ), + OperateFunctionArg::with_name("b", DataType::Varchar(None)), + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + value: "\n BEGIN\n RETURN TRUE;\n END;\n ".to_owned(), + tag: None + })) + .with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let incorrect_sql = "CREATE FUNCTION add(function(struct int64), b INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'"; + assert!(pg().parse_sql_statements(incorrect_sql).is_err(),); +} + #[test] fn parse_create_function() { let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'"; From bf2b72fbe0ae08ec5faeffc6e81e3b830c302c5d Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Fri, 23 May 2025 06:09:05 +0100 Subject: [PATCH 071/130] Mysql: Add `SRID` column option (#1852) --- src/ast/ddl.rs | 10 ++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 4 ++++ tests/sqlparser_mysql.rs | 5 +++++ 5 files changed, 21 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 27089713..a8a1fdba 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1758,6 +1758,13 @@ pub enum ColumnOption { /// ``` /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table Tags(TagsColumnOption), + /// MySQL specific: Spatial reference identifier + /// Syntax: + /// ```sql + /// CREATE TABLE geom (g GEOMETRY NOT NULL SRID 4326); + /// ``` + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/creating-spatial-indexes.html + Srid(Box), } impl fmt::Display for ColumnOption { @@ -1873,6 +1880,9 @@ impl fmt::Display for ColumnOption { Tags(tags) => { write!(f, "{tags}") } + Srid(srid) => { + write!(f, "SRID {srid}") + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index bffd1172..1c28b62c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -871,6 +871,7 @@ impl Spanned for ColumnOption { ColumnOption::OnConflict(..) => Span::empty(), ColumnOption::Policy(..) => Span::empty(), ColumnOption::Tags(..) => Span::empty(), + ColumnOption::Srid(..) => Span::empty(), } } } diff --git a/src/keywords.rs b/src/keywords.rs index aaa2e167..f5c5e567 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -844,6 +844,7 @@ define_keywords!( SQLSTATE, SQLWARNING, SQRT, + SRID, STABLE, STAGE, START, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f6a45ada..47b321d9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7754,6 +7754,10 @@ impl<'a> Parser<'a> { && dialect_of!(self is MySqlDialect | SQLiteDialect | DuckDbDialect | GenericDialect) { self.parse_optional_column_option_as() + } else if self.parse_keyword(Keyword::SRID) + && dialect_of!(self is MySqlDialect | GenericDialect) + { + Ok(Some(ColumnOption::Srid(Box::new(self.parse_expr()?)))) } else if self.parse_keyword(Keyword::IDENTITY) && dialect_of!(self is MsSqlDialect | GenericDialect) { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index dd279894..bcde14ee 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3745,6 +3745,11 @@ fn parse_begin_without_transaction() { mysql().verified_stmt("BEGIN"); } +#[test] +fn parse_geometric_types_srid_option() { + mysql_and_generic().verified_stmt("CREATE TABLE t (a geometry SRID 4326)"); +} + #[test] fn parse_double_precision() { mysql().verified_stmt("CREATE TABLE foo (bar DOUBLE)"); From 301726541a9c34d979bbfe82014752fd656afc15 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 23 May 2025 01:19:16 -0400 Subject: [PATCH 072/130] Add support for table valued functions for SQL Server (#1839) --- src/ast/data_type.rs | 24 ++++++++++- src/ast/ddl.rs | 6 +++ src/ast/mod.rs | 24 +++++++++++ src/parser/mod.rs | 81 ++++++++++++++++++++++++++++--------- tests/sqlparser_mssql.rs | 87 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 21 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 52919de8..3a4958c9 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -48,7 +48,17 @@ pub enum DataType { /// Table type in [PostgreSQL], e.g. CREATE FUNCTION RETURNS TABLE(...). /// /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html - Table(Vec), + /// [MsSQL]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#c-create-a-multi-statement-table-valued-function + Table(Option>), + /// Table type with a name, e.g. CREATE FUNCTION RETURNS @result TABLE(...). + /// + /// [MsSQl]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#table + NamedTable { + /// Table name. + name: ObjectName, + /// Table columns. + columns: Vec, + }, /// Fixed-length character type, e.g. CHARACTER(10). Character(Option), /// Fixed-length char type, e.g. CHAR(10). @@ -716,7 +726,17 @@ impl fmt::Display for DataType { DataType::Unspecified => Ok(()), DataType::Trigger => write!(f, "TRIGGER"), DataType::AnyType => write!(f, "ANY TYPE"), - DataType::Table(fields) => write!(f, "TABLE({})", display_comma_separated(fields)), + DataType::Table(fields) => match fields { + Some(fields) => { + write!(f, "TABLE({})", display_comma_separated(fields)) + } + None => { + write!(f, "TABLE") + } + }, + DataType::NamedTable { name, columns } => { + write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) + } DataType::GeometricType(kind) => write!(f, "{}", kind), } } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a8a1fdba..bbfa7d3c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2356,6 +2356,12 @@ impl fmt::Display for CreateFunction { if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { write!(f, " RETURN {function_body}")?; } + if let Some(CreateFunctionBody::AsReturnExpr(function_body)) = &self.function_body { + write!(f, " AS RETURN {function_body}")?; + } + if let Some(CreateFunctionBody::AsReturnSelect(function_body)) = &self.function_body { + write!(f, " AS RETURN {function_body}")?; + } if let Some(using) = &self.using { write!(f, " {using}")?; } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e18251ea..8e7bec3f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8780,6 +8780,30 @@ pub enum CreateFunctionBody { /// /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html Return(Expr), + + /// Function body expression using the 'AS RETURN' keywords + /// + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(a INT, b INT) + /// RETURNS TABLE + /// AS RETURN (SELECT a + b AS sum); + /// ``` + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql + AsReturnExpr(Expr), + + /// Function body expression using the 'AS RETURN' keywords, with an un-parenthesized SELECT query + /// + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(a INT, b INT) + /// RETURNS TABLE + /// AS RETURN SELECT a + b AS sum; + /// ``` + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#select_stmt + AsReturnSelect(Select), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 47b321d9..4299d156 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5204,19 +5204,62 @@ impl<'a> Parser<'a> { let (name, args) = self.parse_create_function_name_and_params()?; self.expect_keyword(Keyword::RETURNS)?; - let return_type = Some(self.parse_data_type()?); - self.expect_keyword_is(Keyword::AS)?; + let return_table = self.maybe_parse(|p| { + let return_table_name = p.parse_identifier()?; - let begin_token = self.expect_keyword(Keyword::BEGIN)?; - let statements = self.parse_statement_list(&[Keyword::END])?; - let end_token = self.expect_keyword(Keyword::END)?; + p.expect_keyword_is(Keyword::TABLE)?; + p.prev_token(); - let function_body = Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { - begin_token: AttachedToken(begin_token), - statements, - end_token: AttachedToken(end_token), - })); + let table_column_defs = match p.parse_data_type()? { + DataType::Table(Some(table_column_defs)) if !table_column_defs.is_empty() => { + table_column_defs + } + _ => parser_err!( + "Expected table column definitions after TABLE keyword", + p.peek_token().span.start + )?, + }; + + Ok(DataType::NamedTable { + name: ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), + columns: table_column_defs, + }) + })?; + + let return_type = if return_table.is_some() { + return_table + } else { + Some(self.parse_data_type()?) + }; + + let _ = self.parse_keyword(Keyword::AS); + + let function_body = if self.peek_keyword(Keyword::BEGIN) { + let begin_token = self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(&[Keyword::END])?; + let end_token = self.expect_keyword(Keyword::END)?; + + Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + })) + } else if self.parse_keyword(Keyword::RETURN) { + if self.peek_token() == Token::LParen { + Some(CreateFunctionBody::AsReturnExpr(self.parse_expr()?)) + } else if self.peek_keyword(Keyword::SELECT) { + let select = self.parse_select()?; + Some(CreateFunctionBody::AsReturnSelect(select)) + } else { + parser_err!( + "Expected a subquery (or bare SELECT statement) after RETURN", + self.peek_token().span.start + )? + } + } else { + parser_err!("Unparsable function body", self.peek_token().span.start)? + }; Ok(Statement::CreateFunction(CreateFunction { or_alter, @@ -9797,8 +9840,14 @@ impl<'a> Parser<'a> { Ok(DataType::AnyType) } Keyword::TABLE => { - let columns = self.parse_returns_table_columns()?; - Ok(DataType::Table(columns)) + // an LParen after the TABLE keyword indicates that table columns are being defined + // whereas no LParen indicates an anonymous table expression will be returned + if self.peek_token() == Token::LParen { + let columns = self.parse_returns_table_columns()?; + Ok(DataType::Table(Some(columns))) + } else { + Ok(DataType::Table(None)) + } } Keyword::SIGNED => { if self.parse_keyword(Keyword::INTEGER) { @@ -9839,13 +9888,7 @@ impl<'a> Parser<'a> { } fn parse_returns_table_column(&mut self) -> Result { - let name = self.parse_identifier()?; - let data_type = self.parse_data_type()?; - Ok(ColumnDef { - name, - data_type, - options: Vec::new(), // No constraints expected here - }) + self.parse_column_def() } fn parse_returns_table_columns(&mut self) -> Result, ParserError> { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 7b3769ec..32388c44 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -254,6 +254,12 @@ fn parse_create_function() { "; let _ = ms().verified_stmt(multi_statement_function); + let multi_statement_function_without_as = multi_statement_function.replace(" AS", ""); + let _ = ms().one_statement_parses_to( + &multi_statement_function_without_as, + multi_statement_function, + ); + let create_function_with_conditional = "\ CREATE FUNCTION some_scalar_udf() \ RETURNS INT \ @@ -288,6 +294,87 @@ fn parse_create_function() { END\ "; let _ = ms().verified_stmt(create_function_with_return_expression); + + let create_inline_table_value_function = "\ + CREATE FUNCTION some_inline_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS TABLE \ + AS \ + RETURN (SELECT 1 AS col_1)\ + "; + let _ = ms().verified_stmt(create_inline_table_value_function); + + let create_inline_table_value_function_without_parentheses = "\ + CREATE FUNCTION some_inline_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS TABLE \ + AS \ + RETURN SELECT 1 AS col_1\ + "; + let _ = ms().verified_stmt(create_inline_table_value_function_without_parentheses); + + let create_inline_table_value_function_without_as = + create_inline_table_value_function.replace(" AS", ""); + let _ = ms().one_statement_parses_to( + &create_inline_table_value_function_without_as, + create_inline_table_value_function, + ); + + let create_multi_statement_table_value_function = "\ + CREATE FUNCTION some_multi_statement_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS @t TABLE (col_1 INT) \ + AS \ + BEGIN \ + INSERT INTO @t SELECT 1; \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_multi_statement_table_value_function); + + let create_multi_statement_table_value_function_without_as = + create_multi_statement_table_value_function.replace(" AS", ""); + let _ = ms().one_statement_parses_to( + &create_multi_statement_table_value_function_without_as, + create_multi_statement_table_value_function, + ); + + let create_multi_statement_table_value_function_with_constraints = "\ + CREATE FUNCTION some_multi_statement_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS @t TABLE (col_1 INT NOT NULL) \ + AS \ + BEGIN \ + INSERT INTO @t SELECT 1; \ + RETURN @t; \ + END\ + "; + let _ = ms().verified_stmt(create_multi_statement_table_value_function_with_constraints); + + let create_multi_statement_tvf_without_table_definition = "\ + CREATE FUNCTION incorrect_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS @t TABLE () + AS \ + BEGIN \ + INSERT INTO @t SELECT 1; \ + RETURN @t; \ + END\ + "; + assert_eq!( + ParserError::ParserError("Unparsable function body".to_owned()), + ms().parse_sql_statements(create_multi_statement_tvf_without_table_definition) + .unwrap_err() + ); + + let create_inline_tvf_without_subquery_or_bare_select = "\ + CREATE FUNCTION incorrect_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS TABLE + AS \ + RETURN 'hi'\ + "; + assert_eq!( + ParserError::ParserError( + "Expected a subquery (or bare SELECT statement) after RETURN".to_owned() + ), + ms().parse_sql_statements(create_inline_tvf_without_subquery_or_bare_select) + .unwrap_err() + ); } #[test] From 9159d08c5ed4f08457e05878476051e27f6daa34 Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 28 May 2025 13:09:40 +0800 Subject: [PATCH 073/130] Keep the COLUMN keyword only if it exists when dropping the column (#1862) --- src/ast/ddl.rs | 5 ++++- src/ast/spans.rs | 1 + src/parser/mod.rs | 3 ++- tests/sqlparser_common.rs | 5 +++-- tests/sqlparser_mysql.rs | 2 ++ 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index bbfa7d3c..06b85b0f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -139,6 +139,7 @@ pub enum AlterTableOperation { }, /// `DROP [ COLUMN ] [ IF EXISTS ] [ CASCADE ]` DropColumn { + has_column_keyword: bool, column_name: Ident, if_exists: bool, drop_behavior: Option, @@ -606,12 +607,14 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::DropPrimaryKey => write!(f, "DROP PRIMARY KEY"), AlterTableOperation::DropForeignKey { name } => write!(f, "DROP FOREIGN KEY {name}"), AlterTableOperation::DropColumn { + has_column_keyword, column_name, if_exists, drop_behavior, } => write!( f, - "DROP COLUMN {}{}{}", + "DROP {}{}{}{}", + if *has_column_keyword { "COLUMN " } else { "" }, if *if_exists { "IF EXISTS " } else { "" }, column_name, match drop_behavior { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1c28b62c..d612738c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1090,6 +1090,7 @@ impl Spanned for AlterTableOperation { drop_behavior: _, } => name.span, AlterTableOperation::DropColumn { + has_column_keyword: _, column_name, if_exists: _, drop_behavior: _, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4299d156..fcd07aa4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8608,11 +8608,12 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::CLUSTERING, Keyword::KEY]) { AlterTableOperation::DropClusteringKey } else { - let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] + let has_column_keyword = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let column_name = self.parse_identifier()?; let drop_behavior = self.parse_optional_drop_behavior(); AlterTableOperation::DropColumn { + has_column_keyword, column_name, if_exists, drop_behavior, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 86c473d7..d02d7d83 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4926,17 +4926,18 @@ fn parse_alter_table_drop_column() { check_one("DROP COLUMN IF EXISTS is_active CASCADE"); check_one("DROP COLUMN IF EXISTS is_active RESTRICT"); one_statement_parses_to( - "ALTER TABLE tab DROP IF EXISTS is_active CASCADE", + "ALTER TABLE tab DROP COLUMN IF EXISTS is_active CASCADE", "ALTER TABLE tab DROP COLUMN IF EXISTS is_active CASCADE", ); one_statement_parses_to( "ALTER TABLE tab DROP is_active CASCADE", - "ALTER TABLE tab DROP COLUMN is_active CASCADE", + "ALTER TABLE tab DROP is_active CASCADE", ); fn check_one(constraint_text: &str) { match alter_table_op(verified_stmt(&format!("ALTER TABLE tab {constraint_text}"))) { AlterTableOperation::DropColumn { + has_column_keyword: true, column_name, if_exists, drop_behavior, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index bcde14ee..71a5d905 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2801,6 +2801,7 @@ fn parse_alter_table_with_algorithm() { operations, vec![ AlterTableOperation::DropColumn { + has_column_keyword: true, column_name: Ident::new("password_digest"), if_exists: false, drop_behavior: None, @@ -2848,6 +2849,7 @@ fn parse_alter_table_with_lock() { operations, vec![ AlterTableOperation::DropColumn { + has_column_keyword: true, column_name: Ident::new("password_digest"), if_exists: false, drop_behavior: None, From eacf00d269bf3d1323100c33a18f139d6921adb5 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 29 May 2025 05:49:28 -0400 Subject: [PATCH 074/130] Add support for parameter default values in SQL Server (#1866) --- src/parser/mod.rs | 8 +++++++- tests/sqlparser_mssql.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fcd07aa4..c1be8764 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5289,11 +5289,17 @@ impl<'a> Parser<'a> { |parser: &mut Parser| -> Result { let name = parser.parse_identifier()?; let data_type = parser.parse_data_type()?; + let default_expr = if parser.consume_token(&Token::Eq) { + Some(parser.parse_expr()?) + } else { + None + }; + Ok(OperateFunctionArg { mode: None, name: Some(name), data_type, - default_expr: None, + default_expr, }) }; self.expect_token(&Token::LParen)?; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 32388c44..2a314502 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -377,6 +377,46 @@ fn parse_create_function() { ); } +#[test] +fn parse_create_function_parameter_default_values() { + let single_default_sql = + "CREATE FUNCTION test_func(@param1 INT = 42) RETURNS INT AS BEGIN RETURN @param1; END"; + assert_eq!( + ms().verified_stmt(single_default_sql), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: false, + temporary: false, + if_not_exists: false, + name: ObjectName::from(vec![Ident::new("test_func")]), + args: Some(vec![OperateFunctionArg { + mode: None, + name: Some(Ident::new("@param1")), + data_type: DataType::Int(None), + default_expr: Some(Expr::Value((number("42")).with_empty_span())), + },]), + return_type: Some(DataType::Int(None)), + function_body: Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { + begin_token: AttachedToken::empty(), + statements: vec![Statement::Return(ReturnStatement { + value: Some(ReturnStatementValue::Expr(Expr::Identifier(Ident::new( + "@param1" + )))), + })], + end_token: AttachedToken::empty(), + })), + behavior: None, + called_on_null: None, + parallel: None, + using: None, + language: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }), + ); +} + #[test] fn parse_mssql_apply_join() { let _ = ms_and_generic().verified_only_select( From a8bde39efb4c3568fb3dc685b440962d50403fc3 Mon Sep 17 00:00:00 2001 From: Hendrik Makait Date: Fri, 30 May 2025 09:14:36 +0200 Subject: [PATCH 075/130] Add support for `TABLESAMPLE` pipe operator (#1860) --- src/ast/query.rs | 14 +++++++++++--- src/parser/mod.rs | 15 +++++++++++++-- tests/sqlparser_common.rs | 5 +++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 5b784b19..ffe1e402 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1559,7 +1559,7 @@ impl fmt::Display for TableSampleBucket { } impl fmt::Display for TableSample { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, " {}", self.modifier)?; + write!(f, "{}", self.modifier)?; if let Some(name) = &self.name { write!(f, " {}", name)?; } @@ -1862,7 +1862,7 @@ impl fmt::Display for TableFactor { write!(f, " WITH ORDINALITY")?; } if let Some(TableSampleKind::BeforeTableAlias(sample)) = sample { - write!(f, "{sample}")?; + write!(f, " {sample}")?; } if let Some(alias) = alias { write!(f, " AS {alias}")?; @@ -1877,7 +1877,7 @@ impl fmt::Display for TableFactor { write!(f, "{version}")?; } if let Some(TableSampleKind::AfterTableAlias(sample)) = sample { - write!(f, "{sample}")?; + write!(f, " {sample}")?; } Ok(()) } @@ -2680,6 +2680,10 @@ pub enum PipeOperator { full_table_exprs: Vec, group_by_expr: Vec, }, + /// Selects a random sample of rows from the input table. + /// Syntax: `|> TABLESAMPLE SYSTEM (10 PERCENT) + /// See more at + TableSample { sample: Box }, } impl fmt::Display for PipeOperator { @@ -2731,6 +2735,10 @@ impl fmt::Display for PipeOperator { PipeOperator::OrderBy { exprs } => { write!(f, "ORDER BY {}", display_comma_separated(exprs.as_slice())) } + + PipeOperator::TableSample { sample } => { + write!(f, "{}", sample) + } } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c1be8764..6d642384 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11054,6 +11054,7 @@ impl<'a> Parser<'a> { Keyword::LIMIT, Keyword::AGGREGATE, Keyword::ORDER, + Keyword::TABLESAMPLE, ])?; match kw { Keyword::SELECT => { @@ -11116,6 +11117,10 @@ impl<'a> Parser<'a> { let exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; pipe_operators.push(PipeOperator::OrderBy { exprs }) } + Keyword::TABLESAMPLE => { + let sample = self.parse_table_sample(TableSampleModifier::TableSample)?; + pipe_operators.push(PipeOperator::TableSample { sample }); + } unhandled => { return Err(ParserError::ParserError(format!( "`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}" @@ -12760,7 +12765,13 @@ impl<'a> Parser<'a> { } else { return Ok(None); }; + self.parse_table_sample(modifier).map(Some) + } + fn parse_table_sample( + &mut self, + modifier: TableSampleModifier, + ) -> Result, ParserError> { let name = match self.parse_one_of_keywords(&[ Keyword::BERNOULLI, Keyword::ROW, @@ -12842,14 +12853,14 @@ impl<'a> Parser<'a> { None }; - Ok(Some(Box::new(TableSample { + Ok(Box::new(TableSample { modifier, name, quantity, seed, bucket, offset, - }))) + })) } fn parse_table_sample_seed( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d02d7d83..1cc79317 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15156,6 +15156,11 @@ fn parse_pipeline_operator() { dialects.verified_stmt("SELECT * FROM users |> ORDER BY id DESC"); dialects.verified_stmt("SELECT * FROM users |> ORDER BY id DESC, name ASC"); + // tablesample pipe operator + dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE BERNOULLI (50)"); + dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50 PERCENT)"); + dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50) REPEATABLE (10)"); + // many pipes dialects.verified_stmt( "SELECT * FROM CustomerOrders |> AGGREGATE SUM(cost) AS total_cost GROUP BY customer_id, state, item_type |> EXTEND COUNT(*) OVER (PARTITION BY customer_id) AS num_orders |> WHERE num_orders > 1 |> AGGREGATE AVG(total_cost) AS average GROUP BY state DESC, item_type ASC", From 80d47eee84f0954ad24918b046e47f08e3762142 Mon Sep 17 00:00:00 2001 From: Dmitriy Mazurin Date: Fri, 30 May 2025 08:16:36 +0100 Subject: [PATCH 076/130] Adds support for mysql's drop index (#1864) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 32 +++++++++++++++++++++----------- src/parser/mod.rs | 6 ++++++ tests/sqlparser_mysql.rs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e7bec3f..653f58e4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3405,6 +3405,9 @@ pub enum Statement { purge: bool, /// MySQL-specific "TEMPORARY" keyword temporary: bool, + /// MySQL-specific drop index syntax, which requires table specification + /// See + table: Option, }, /// ```sql /// DROP FUNCTION @@ -5242,17 +5245,24 @@ impl fmt::Display for Statement { restrict, purge, temporary, - } => write!( - f, - "DROP {}{}{} {}{}{}{}", - if *temporary { "TEMPORARY " } else { "" }, - object_type, - if *if_exists { " IF EXISTS" } else { "" }, - display_comma_separated(names), - if *cascade { " CASCADE" } else { "" }, - if *restrict { " RESTRICT" } else { "" }, - if *purge { " PURGE" } else { "" } - ), + table, + } => { + write!( + f, + "DROP {}{}{} {}{}{}{}", + if *temporary { "TEMPORARY " } else { "" }, + object_type, + if *if_exists { " IF EXISTS" } else { "" }, + display_comma_separated(names), + if *cascade { " CASCADE" } else { "" }, + if *restrict { " RESTRICT" } else { "" }, + if *purge { " PURGE" } else { "" }, + )?; + if let Some(table_name) = table.as_ref() { + write!(f, " ON {}", table_name)?; + }; + Ok(()) + } Statement::DropFunction { if_exists, func_desc, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6d642384..f2da659f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6255,6 +6255,11 @@ impl<'a> Parser<'a> { loc ); } + let table = if self.parse_keyword(Keyword::ON) { + Some(self.parse_object_name(false)?) + } else { + None + }; Ok(Statement::Drop { object_type, if_exists, @@ -6263,6 +6268,7 @@ impl<'a> Parser<'a> { restrict, purge, temporary, + table, }) } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 71a5d905..337707c9 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3987,3 +3987,34 @@ fn parse_straight_join() { mysql() .verified_stmt("SELECT a.*, b.* FROM table_a STRAIGHT_JOIN table_b AS b ON a.b_id = b.id"); } + +#[test] +fn parse_drop_index() { + let sql = "DROP INDEX idx_name ON table_name"; + match mysql().verified_stmt(sql) { + Statement::Drop { + object_type, + if_exists, + names, + cascade, + restrict, + purge, + temporary, + table, + } => { + assert!(!if_exists); + assert_eq!(ObjectType::Index, object_type); + assert_eq!( + vec!["idx_name"], + names.iter().map(ToString::to_string).collect::>() + ); + assert!(!cascade); + assert!(!restrict); + assert!(!purge); + assert!(!temporary); + assert!(table.is_some()); + assert_eq!("table_name", table.unwrap().to_string()); + } + _ => unreachable!(), + } +} From 394a53448678ef16b715482e4b643badd035fb5e Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Mon, 2 Jun 2025 19:04:35 +0300 Subject: [PATCH 077/130] Fix: GROUPING SETS accept values without parenthesis (#1867) --- src/parser/mod.rs | 8 +++++++- tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f2da659f..a28540d1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10111,7 +10111,13 @@ impl<'a> Parser<'a> { } if self.parse_keywords(&[Keyword::GROUPING, Keyword::SETS]) { self.expect_token(&Token::LParen)?; - let result = self.parse_comma_separated(|p| p.parse_tuple(true, true))?; + let result = self.parse_comma_separated(|p| { + if p.peek_token_ref().token == Token::LParen { + p.parse_tuple(true, true) + } else { + Ok(vec![p.parse_expr()?]) + } + })?; self.expect_token(&Token::RParen)?; modifiers.push(GroupByWithModifier::GroupingSets(Expr::GroupingSets( result, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1cc79317..a1a8fc3b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2822,6 +2822,38 @@ fn parse_group_by_special_grouping_sets() { } } +#[test] +fn parse_group_by_grouping_sets_single_values() { + let sql = "SELECT a, b, SUM(c) FROM tab1 GROUP BY a, b GROUPING SETS ((a, b), a, (b), c, ())"; + let canonical = + "SELECT a, b, SUM(c) FROM tab1 GROUP BY a, b GROUPING SETS ((a, b), (a), (b), (c), ())"; + match all_dialects().one_statement_parses_to(sql, canonical) { + Statement::Query(query) => { + let group_by = &query.body.as_select().unwrap().group_by; + assert_eq!( + group_by, + &GroupByExpr::Expressions( + vec![ + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")) + ], + vec![GroupByWithModifier::GroupingSets(Expr::GroupingSets(vec![ + vec![ + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")) + ], + vec![Expr::Identifier(Ident::new("a"))], + vec![Expr::Identifier(Ident::new("b"))], + vec![Expr::Identifier(Ident::new("c"))], + vec![] + ]))] + ) + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_select_having() { let sql = "SELECT foo FROM bar GROUP BY foo HAVING COUNT(*) > 1"; From 5327f0ce132e12de71db7d03711397c5ac6c0031 Mon Sep 17 00:00:00 2001 From: Artem Osipov <59066880+osipovartem@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:49:07 +0300 Subject: [PATCH 078/130] Add ICEBERG keyword support to ALTER TABLE statement (#1869) --- src/ast/mod.rs | 13 ++++++-- src/ast/spans.rs | 1 + src/parser/mod.rs | 65 ++++++++++++++++++++---------------- src/test_utils.rs | 2 ++ tests/sqlparser_mysql.rs | 8 ++--- tests/sqlparser_postgres.rs | 6 ++-- tests/sqlparser_snowflake.rs | 7 ++++ 7 files changed, 64 insertions(+), 38 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 653f58e4..711e580d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3281,6 +3281,9 @@ pub enum Statement { /// For example: `ALTER TABLE table_name ON CLUSTER cluster_name ADD COLUMN c UInt32` /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) on_cluster: Option, + /// Snowflake "ICEBERG" clause for Iceberg tables + /// + iceberg: bool, }, /// ```sql /// ALTER INDEX @@ -3405,7 +3408,7 @@ pub enum Statement { purge: bool, /// MySQL-specific "TEMPORARY" keyword temporary: bool, - /// MySQL-specific drop index syntax, which requires table specification + /// MySQL-specific drop index syntax, which requires table specification /// See table: Option, }, @@ -5139,8 +5142,14 @@ impl fmt::Display for Statement { operations, location, on_cluster, + iceberg, } => { - write!(f, "ALTER TABLE ")?; + if *iceberg { + write!(f, "ALTER ICEBERG TABLE ")?; + } else { + write!(f, "ALTER TABLE ")?; + } + if *if_exists { write!(f, "IF EXISTS ")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d612738c..dd918c34 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -431,6 +431,7 @@ impl Spanned for Statement { operations, location: _, on_cluster, + iceberg: _, } => union_spans( core::iter::once(name.span()) .chain(operations.iter().map(|i| i.span())) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a28540d1..3e721072 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8893,38 +8893,15 @@ impl<'a> Parser<'a> { Keyword::ROLE, Keyword::POLICY, Keyword::CONNECTOR, + Keyword::ICEBERG, ])?; match object_type { Keyword::VIEW => self.parse_alter_view(), Keyword::TYPE => self.parse_alter_type(), - Keyword::TABLE => { - let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] - let table_name = self.parse_object_name(false)?; - let on_cluster = self.parse_optional_on_cluster()?; - let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; - - let mut location = None; - if self.parse_keyword(Keyword::LOCATION) { - location = Some(HiveSetLocation { - has_set: false, - location: self.parse_identifier()?, - }); - } else if self.parse_keywords(&[Keyword::SET, Keyword::LOCATION]) { - location = Some(HiveSetLocation { - has_set: true, - location: self.parse_identifier()?, - }); - } - - Ok(Statement::AlterTable { - name: table_name, - if_exists, - only, - operations, - location, - on_cluster, - }) + Keyword::TABLE => self.parse_alter_table(false), + Keyword::ICEBERG => { + self.expect_keyword(Keyword::TABLE)?; + self.parse_alter_table(true) } Keyword::INDEX => { let index_name = self.parse_object_name(false)?; @@ -8952,6 +8929,38 @@ impl<'a> Parser<'a> { } } + /// Parse a [Statement::AlterTable] + pub fn parse_alter_table(&mut self, iceberg: bool) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] + let table_name = self.parse_object_name(false)?; + let on_cluster = self.parse_optional_on_cluster()?; + let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; + + let mut location = None; + if self.parse_keyword(Keyword::LOCATION) { + location = Some(HiveSetLocation { + has_set: false, + location: self.parse_identifier()?, + }); + } else if self.parse_keywords(&[Keyword::SET, Keyword::LOCATION]) { + location = Some(HiveSetLocation { + has_set: true, + location: self.parse_identifier()?, + }); + } + + Ok(Statement::AlterTable { + name: table_name, + if_exists, + only, + operations, + location, + on_cluster, + iceberg, + }) + } + pub fn parse_alter_view(&mut self) -> Result { let name = self.parse_object_name(false)?; let columns = self.parse_parenthesized_column_list(Optional, false)?; diff --git a/src/test_utils.rs b/src/test_utils.rs index 3c22fa91..24c0ca57 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -345,10 +345,12 @@ pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTa operations, on_cluster: _, location: _, + iceberg, } => { assert_eq!(name.to_string(), expected_name); assert!(!if_exists); assert!(!is_only); + assert!(!iceberg); only(operations) } _ => panic!("Expected ALTER TABLE statement"), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 337707c9..0800c329 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2507,11 +2507,13 @@ fn parse_alter_table_add_column() { if_exists, only, operations, + iceberg, location: _, on_cluster: _, } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); + assert!(!iceberg); assert!(!only); assert_eq!( operations, @@ -2536,8 +2538,7 @@ fn parse_alter_table_add_column() { if_exists, only, operations, - location: _, - on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); @@ -2574,8 +2575,7 @@ fn parse_alter_table_add_columns() { if_exists, only, operations, - location: _, - on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 682c0d6c..d54d4e2a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -834,8 +834,7 @@ fn parse_alter_table_add_columns() { if_exists, only, operations, - location: _, - on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert!(if_exists); @@ -915,8 +914,7 @@ fn parse_alter_table_owner_to() { if_exists: _, only: _, operations, - location: _, - on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert_eq!( diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 52be3143..b4d62506 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1591,6 +1591,13 @@ fn test_alter_table_clustering() { snowflake_and_generic().verified_stmt("ALTER TABLE tbl RESUME RECLUSTER"); } +#[test] +fn test_alter_iceberg_table() { + snowflake_and_generic().verified_stmt("ALTER ICEBERG TABLE tbl DROP CLUSTERING KEY"); + snowflake_and_generic().verified_stmt("ALTER ICEBERG TABLE tbl SUSPEND RECLUSTER"); + snowflake_and_generic().verified_stmt("ALTER ICEBERG TABLE tbl RESUME RECLUSTER"); +} + #[test] fn test_drop_stage() { match snowflake_and_generic().verified_stmt("DROP STAGE s1") { From de2cc7b50233a5e5c0a5c9fe7ab08d23b18b9d5c Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Fri, 6 Jun 2025 08:03:59 +0100 Subject: [PATCH 079/130] MySQL: Support `index_name` in FK constraints (#1871) --- src/ast/ddl.rs | 7 ++++++- src/ast/spans.rs | 2 ++ src/parser/mod.rs | 14 ++++++++------ tests/sqlparser_common.rs | 8 ++++++++ tests/sqlparser_mysql.rs | 7 +++++++ 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 06b85b0f..bbc15704 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1019,6 +1019,9 @@ pub enum TableConstraint { /// }`). ForeignKey { name: Option, + /// MySQL-specific field + /// + index_name: Option, columns: Vec, foreign_table: ObjectName, referred_columns: Vec, @@ -1129,6 +1132,7 @@ impl fmt::Display for TableConstraint { } TableConstraint::ForeignKey { name, + index_name, columns, foreign_table, referred_columns, @@ -1138,8 +1142,9 @@ impl fmt::Display for TableConstraint { } => { write!( f, - "{}FOREIGN KEY ({}) REFERENCES {}", + "{}FOREIGN KEY{} ({}) REFERENCES {}", display_constraint_name(name), + display_option_spaced(index_name), display_comma_separated(columns), foreign_table, )?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index dd918c34..a1bad2c5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -671,6 +671,7 @@ impl Spanned for TableConstraint { TableConstraint::ForeignKey { name, columns, + index_name, foreign_table, referred_columns, on_delete, @@ -679,6 +680,7 @@ impl Spanned for TableConstraint { } => union_spans( name.iter() .map(|i| i.span) + .chain(index_name.iter().map(|i| i.span)) .chain(columns.iter().map(|i| i.span)) .chain(core::iter::once(foreign_table.span())) .chain(referred_columns.iter().map(|i| i.span)) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3e721072..677566c5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8061,7 +8061,7 @@ impl<'a> Parser<'a> { let nulls_distinct = self.parse_optional_nulls_distinct()?; // optional index name - let index_name = self.parse_optional_indent()?; + let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -8083,7 +8083,7 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::KEY)?; // optional index name - let index_name = self.parse_optional_indent()?; + let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -8100,6 +8100,7 @@ impl<'a> Parser<'a> { } Token::Word(w) if w.keyword == Keyword::FOREIGN => { self.expect_keyword_is(Keyword::KEY)?; + let index_name = self.parse_optional_ident()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; self.expect_keyword_is(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name(false)?; @@ -8122,6 +8123,7 @@ impl<'a> Parser<'a> { Ok(Some(TableConstraint::ForeignKey { name, + index_name, columns, foreign_table, referred_columns, @@ -8145,7 +8147,7 @@ impl<'a> Parser<'a> { let name = match self.peek_token().token { Token::Word(word) if word.keyword == Keyword::USING => None, - _ => self.parse_optional_indent()?, + _ => self.parse_optional_ident()?, }; let index_type = self.parse_optional_using_then_index_type()?; @@ -8176,7 +8178,7 @@ impl<'a> Parser<'a> { let index_type_display = self.parse_index_type_display(); - let opt_index_name = self.parse_optional_indent()?; + let opt_index_name = self.parse_optional_ident()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -8286,7 +8288,7 @@ impl<'a> Parser<'a> { /// Parse `[ident]`, mostly `ident` is name, like: /// `window_name`, `index_name`, ... - pub fn parse_optional_indent(&mut self) -> Result, ParserError> { + pub fn parse_optional_ident(&mut self) -> Result, ParserError> { self.maybe_parse(|parser| parser.parse_identifier()) } @@ -15698,7 +15700,7 @@ impl<'a> Parser<'a> { pub fn parse_window_spec(&mut self) -> Result { let window_name = match self.peek_token().token { Token::Word(word) if word.keyword == Keyword::NoKeyword => { - self.parse_optional_indent()? + self.parse_optional_ident()? } _ => None, }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a1a8fc3b..0431c720 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3791,6 +3791,7 @@ fn parse_create_table() { vec![ TableConstraint::ForeignKey { name: Some("fkey".into()), + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], @@ -3800,6 +3801,7 @@ fn parse_create_table() { }, TableConstraint::ForeignKey { name: Some("fkey2".into()), + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], @@ -3809,6 +3811,7 @@ fn parse_create_table() { }, TableConstraint::ForeignKey { name: None, + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], @@ -3818,6 +3821,7 @@ fn parse_create_table() { }, TableConstraint::ForeignKey { name: None, + index_name: None, columns: vec!["lng".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], @@ -3914,6 +3918,7 @@ fn parse_create_table_with_constraint_characteristics() { vec![ TableConstraint::ForeignKey { name: Some("fkey".into()), + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], @@ -3927,6 +3932,7 @@ fn parse_create_table_with_constraint_characteristics() { }, TableConstraint::ForeignKey { name: Some("fkey2".into()), + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], @@ -3940,6 +3946,7 @@ fn parse_create_table_with_constraint_characteristics() { }, TableConstraint::ForeignKey { name: None, + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], @@ -3953,6 +3960,7 @@ fn parse_create_table_with_constraint_characteristics() { }, TableConstraint::ForeignKey { name: None, + index_name: None, columns: vec!["lng".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 0800c329..b1b7d539 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3988,6 +3988,13 @@ fn parse_straight_join() { .verified_stmt("SELECT a.*, b.* FROM table_a STRAIGHT_JOIN table_b AS b ON a.b_id = b.id"); } +#[test] +fn mysql_foreign_key_with_index_name() { + mysql().verified_stmt( + "CREATE TABLE orders (customer_id INT, INDEX idx_customer (customer_id), CONSTRAINT fk_customer FOREIGN KEY idx_customer (customer_id) REFERENCES customers(id))", + ); +} + #[test] fn parse_drop_index() { let sql = "DROP INDEX idx_name ON table_name"; From 4cf5e571d3baad070ca76b5e17c9e145af0e3b1a Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Fri, 6 Jun 2025 08:10:03 +0100 Subject: [PATCH 080/130] Postgres: Apply `ONLY` keyword per table in TRUNCATE stmt (#1872) --- src/ast/mod.rs | 14 ++++++++------ src/ast/spans.rs | 1 - src/parser/mod.rs | 8 ++++---- tests/sqlparser_common.rs | 28 ++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 7 ++++--- 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 711e580d..3ccce061 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3014,9 +3014,6 @@ pub enum Statement { /// TABLE - optional keyword; table: bool, /// Postgres-specific option - /// [ TRUNCATE TABLE ONLY ] - only: bool, - /// Postgres-specific option /// [ RESTART IDENTITY | CONTINUE IDENTITY ] identity: Option, /// Postgres-specific option @@ -4425,17 +4422,15 @@ impl fmt::Display for Statement { table_names, partitions, table, - only, identity, cascade, on_cluster, } => { let table = if *table { "TABLE " } else { "" }; - let only = if *only { "ONLY " } else { "" }; write!( f, - "TRUNCATE {table}{only}{table_names}", + "TRUNCATE {table}{table_names}", table_names = display_comma_separated(table_names) )?; @@ -6106,10 +6101,17 @@ pub struct TruncateTableTarget { /// name of the table being truncated #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub name: ObjectName, + /// Postgres-specific option + /// [ TRUNCATE TABLE ONLY ] + /// + pub only: bool, } impl fmt::Display for TruncateTableTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.only { + write!(f, "ONLY ")?; + }; write!(f, "{}", self.name) } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a1bad2c5..11986f8c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -311,7 +311,6 @@ impl Spanned for Statement { table_names, partitions, table: _, - only: _, identity: _, cascade: _, on_cluster: _, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 677566c5..7c9be198 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -960,12 +960,13 @@ impl<'a> Parser<'a> { pub fn parse_truncate(&mut self) -> Result { let table = self.parse_keyword(Keyword::TABLE); - let only = self.parse_keyword(Keyword::ONLY); let table_names = self - .parse_comma_separated(|p| p.parse_object_name(false))? + .parse_comma_separated(|p| { + Ok((p.parse_keyword(Keyword::ONLY), p.parse_object_name(false)?)) + })? .into_iter() - .map(|n| TruncateTableTarget { name: n }) + .map(|(only, name)| TruncateTableTarget { name, only }) .collect(); let mut partitions = None; @@ -996,7 +997,6 @@ impl<'a> Parser<'a> { table_names, partitions, table, - only, identity, cascade, on_cluster, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0431c720..2cb51de3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15302,3 +15302,31 @@ fn test_open() { }) ); } + +#[test] +fn parse_truncate_only() { + let truncate = all_dialects().verified_stmt("TRUNCATE TABLE employee, ONLY dept"); + + let table_names = vec![ + TruncateTableTarget { + name: ObjectName::from(vec![Ident::new("employee")]), + only: false, + }, + TruncateTableTarget { + name: ObjectName::from(vec![Ident::new("dept")]), + only: true, + }, + ]; + + assert_eq!( + Statement::Truncate { + table_names, + partitions: None, + table: true, + identity: None, + cascade: None, + on_cluster: None, + }, + truncate + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d54d4e2a..c50f066a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4788,13 +4788,13 @@ fn parse_truncate() { let table_name = ObjectName::from(vec![Ident::new("db"), Ident::new("table_name")]); let table_names = vec![TruncateTableTarget { name: table_name.clone(), + only: false, }]; assert_eq!( Statement::Truncate { table_names, partitions: None, table: false, - only: false, identity: None, cascade: None, on_cluster: None, @@ -4811,6 +4811,7 @@ fn parse_truncate_with_options() { let table_name = ObjectName::from(vec![Ident::new("db"), Ident::new("table_name")]); let table_names = vec![TruncateTableTarget { name: table_name.clone(), + only: true, }]; assert_eq!( @@ -4818,7 +4819,6 @@ fn parse_truncate_with_options() { table_names, partitions: None, table: true, - only: true, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, @@ -4839,9 +4839,11 @@ fn parse_truncate_with_table_list() { let table_names = vec![ TruncateTableTarget { name: table_name_a.clone(), + only: false, }, TruncateTableTarget { name: table_name_b.clone(), + only: false, }, ]; @@ -4850,7 +4852,6 @@ fn parse_truncate_with_table_list() { table_names, partitions: None, table: true, - only: false, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, From e2b1ae36e94a8cb6d99234a2d9a25f56a3501f30 Mon Sep 17 00:00:00 2001 From: Chen Chongchen Date: Fri, 6 Jun 2025 15:11:44 +0800 Subject: [PATCH 081/130] feat: Hive: support `SORT BY` direction (#1873) --- src/ast/query.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_hive.rs | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index ffe1e402..4398531c 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -341,7 +341,7 @@ pub struct Select { /// DISTRIBUTE BY (Hive) pub distribute_by: Vec, /// SORT BY (Hive) - pub sort_by: Vec, + pub sort_by: Vec, /// HAVING pub having: Option, /// WINDOW AS diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7c9be198..821ce843 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11591,7 +11591,7 @@ impl<'a> Parser<'a> { }; let sort_by = if self.parse_keywords(&[Keyword::SORT, Keyword::BY]) { - self.parse_comma_separated(Parser::parse_expr)? + self.parse_comma_separated(Parser::parse_order_by_expr)? } else { vec![] }; diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 14dcbffd..fd52b773 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -341,6 +341,9 @@ fn lateral_view() { fn sort_by() { let sort_by = "SELECT * FROM db.table SORT BY a"; hive().verified_stmt(sort_by); + + let sort_by_with_direction = "SELECT * FROM db.table SORT BY a, b DESC"; + hive().verified_stmt(sort_by_with_direction); } #[test] From ff29dd25b2b8f4b10dd958f0c601f98d1c51ab36 Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Fri, 6 Jun 2025 16:06:33 +0200 Subject: [PATCH 082/130] Fix `CASE` expression spans (#1874) --- src/ast/mod.rs | 4 ++++ src/ast/spans.rs | 34 ++++++++++++++++++++++++++-------- src/parser/mod.rs | 5 ++++- tests/sqlparser_common.rs | 6 ++++++ tests/sqlparser_databricks.rs | 3 +++ 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3ccce061..6f47ae7f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -967,6 +967,8 @@ pub enum Expr { /// not `< 0` nor `1, 2, 3` as allowed in a `` per /// Case { + case_token: AttachedToken, + end_token: AttachedToken, operand: Option>, conditions: Vec, else_result: Option>, @@ -1675,6 +1677,8 @@ impl fmt::Display for Expr { } Expr::Function(fun) => fun.fmt(f), Expr::Case { + case_token: _, + end_token: _, operand, conditions, else_result, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 11986f8c..f957194a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1567,18 +1567,24 @@ impl Spanned for Expr { ), Expr::Prefixed { value, .. } => value.span(), Expr::Case { + case_token, + end_token, operand, conditions, else_result, } => union_spans( - operand - .as_ref() - .map(|i| i.span()) - .into_iter() - .chain(conditions.iter().flat_map(|case_when| { - [case_when.condition.span(), case_when.result.span()] - })) - .chain(else_result.as_ref().map(|i| i.span())), + iter::once(case_token.0.span) + .chain( + operand + .as_ref() + .map(|i| i.span()) + .into_iter() + .chain(conditions.iter().flat_map(|case_when| { + [case_when.condition.span(), case_when.result.span()] + })) + .chain(else_result.as_ref().map(|i| i.span())), + ) + .chain(iter::once(end_token.0.span)), ), Expr::Exists { subquery, .. } => subquery.span(), Expr::Subquery(query) => query.span(), @@ -2464,4 +2470,16 @@ pub mod tests { assert_eq!(test.get_source(body_span), "SELECT cte.* FROM cte"); } + + #[test] + fn test_case_expr_span() { + let dialect = &GenericDialect; + let mut test = SpanTest::new(dialect, "CASE 1 WHEN 2 THEN 3 ELSE 4 END"); + let expr = test.0.parse_expr().unwrap(); + let expr_span = expr.span(); + assert_eq!( + test.get_source(expr_span), + "CASE 1 WHEN 2 THEN 3 ELSE 4 END" + ); + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 821ce843..f8c307dc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2274,6 +2274,7 @@ impl<'a> Parser<'a> { } pub fn parse_case_expr(&mut self) -> Result { + let case_token = AttachedToken(self.get_current_token().clone()); let mut operand = None; if !self.parse_keyword(Keyword::WHEN) { operand = Some(Box::new(self.parse_expr()?)); @@ -2294,8 +2295,10 @@ impl<'a> Parser<'a> { } else { None }; - self.expect_keyword_is(Keyword::END)?; + let end_token = AttachedToken(self.expect_keyword(Keyword::END)?); Ok(Expr::Case { + case_token, + end_token, operand, conditions, else_result, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2cb51de3..5b96dcd7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6869,6 +6869,8 @@ fn parse_searched_case_expr() { let select = verified_only_select(sql); assert_eq!( &Case { + case_token: AttachedToken::empty(), + end_token: AttachedToken::empty(), operand: None, conditions: vec![ CaseWhen { @@ -6908,6 +6910,8 @@ fn parse_simple_case_expr() { use self::Expr::{Case, Identifier}; assert_eq!( &Case { + case_token: AttachedToken::empty(), + end_token: AttachedToken::empty(), operand: Some(Box::new(Identifier(Ident::new("foo")))), conditions: vec![CaseWhen { condition: Expr::value(number("1")), @@ -14650,6 +14654,8 @@ fn test_lambdas() { Expr::Lambda(LambdaFunction { params: OneOrManyWithParens::Many(vec![Ident::new("p1"), Ident::new("p2")]), body: Box::new(Expr::Case { + case_token: AttachedToken::empty(), + end_token: AttachedToken::empty(), operand: None, conditions: vec![ CaseWhen { diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 88aae499..99b7eecd 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +use sqlparser::ast::helpers::attached_token::AttachedToken; use sqlparser::ast::*; use sqlparser::dialect::{DatabricksDialect, GenericDialect}; use sqlparser::parser::ParserError; @@ -108,6 +109,8 @@ fn test_databricks_lambdas() { Expr::Lambda(LambdaFunction { params: OneOrManyWithParens::Many(vec![Ident::new("p1"), Ident::new("p2")]), body: Box::new(Expr::Case { + case_token: AttachedToken::empty(), + end_token: AttachedToken::empty(), operand: None, conditions: vec![ CaseWhen { From 84c3a1b325c39c879b68ab712e3b9b3e3e40ed56 Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Sat, 7 Jun 2025 05:48:40 +0100 Subject: [PATCH 083/130] MySQL: `[[NOT] ENFORCED]` in CHECK constraint (#1870) --- src/ast/ddl.rs | 18 +++++++++++++++--- src/ast/spans.rs | 8 +++++--- src/parser/mod.rs | 15 ++++++++++++++- tests/sqlparser_common.rs | 7 +++++++ tests/sqlparser_postgres.rs | 5 +++++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index bbc15704..b0a3708c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1029,10 +1029,13 @@ pub enum TableConstraint { on_update: Option, characteristics: Option, }, - /// `[ CONSTRAINT ] CHECK ()` + /// `[ CONSTRAINT ] CHECK () [[NOT] ENFORCED]` Check { name: Option, expr: Box, + /// MySQL-specific syntax + /// + enforced: Option, }, /// MySQLs [index definition][1] for index creation. Not present on ANSI so, for now, the usage /// is restricted to MySQL, as no other dialects that support this syntax were found. @@ -1162,8 +1165,17 @@ impl fmt::Display for TableConstraint { } Ok(()) } - TableConstraint::Check { name, expr } => { - write!(f, "{}CHECK ({})", display_constraint_name(name), expr) + TableConstraint::Check { + name, + expr, + enforced, + } => { + write!(f, "{}CHECK ({})", display_constraint_name(name), expr)?; + if let Some(b) = enforced { + write!(f, " {}", if *b { "ENFORCED" } else { "NOT ENFORCED" }) + } else { + Ok(()) + } } TableConstraint::Index { display_as_key, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f957194a..39d30df9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -687,9 +687,11 @@ impl Spanned for TableConstraint { .chain(on_update.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), - TableConstraint::Check { name, expr } => { - expr.span().union_opt(&name.as_ref().map(|i| i.span)) - } + TableConstraint::Check { + name, + expr, + enforced: _, + } => expr.span().union_opt(&name.as_ref().map(|i| i.span)), TableConstraint::Index { display_as_key: _, name, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f8c307dc..9cef22ed 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8139,7 +8139,20 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let expr = Box::new(self.parse_expr()?); self.expect_token(&Token::RParen)?; - Ok(Some(TableConstraint::Check { name, expr })) + + let enforced = if self.parse_keyword(Keyword::ENFORCED) { + Some(true) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::ENFORCED]) { + Some(false) + } else { + None + }; + + Ok(Some(TableConstraint::Check { + name, + expr, + enforced, + })) } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5b96dcd7..399fdb3d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15336,3 +15336,10 @@ fn parse_truncate_only() { truncate ); } + +#[test] +fn check_enforced() { + all_dialects().verified_stmt( + "CREATE TABLE t (a INT, b INT, c INT, CHECK (a > 0) NOT ENFORCED, CHECK (b > 0) ENFORCED, CHECK (c > 0))", + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c50f066a..6f0ba9c6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5378,6 +5378,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + enforced: None, }], }); @@ -5396,6 +5397,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + enforced: None, }], }); @@ -5414,6 +5416,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + enforced: None, }], }); @@ -5432,6 +5435,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + enforced: None, }], }); @@ -5450,6 +5454,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + enforced: None, }], }); From 40d12b98bd195eecf8f1b89dcfd9d163c96fd0af Mon Sep 17 00:00:00 2001 From: Yannick Utard Date: Tue, 10 Jun 2025 06:50:10 +0200 Subject: [PATCH 084/130] Add support for `CREATE SCHEMA WITH ( )` (#1877) --- src/ast/mod.rs | 13 +++++++++++++ src/parser/mod.rs | 7 +++++++ tests/sqlparser_common.rs | 3 +++ 3 files changed, 23 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6f47ae7f..a1d8ff6f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3725,6 +3725,14 @@ pub enum Statement { /// ` | AUTHORIZATION | AUTHORIZATION ` schema_name: SchemaName, if_not_exists: bool, + /// Schema properties. + /// + /// ```sql + /// CREATE SCHEMA myschema WITH (key1='value1'); + /// ``` + /// + /// [Trino](https://trino.io/docs/current/sql/create-schema.html) + with: Option>, /// Schema options. /// /// ```sql @@ -5585,6 +5593,7 @@ impl fmt::Display for Statement { Statement::CreateSchema { schema_name, if_not_exists, + with, options, default_collate_spec, } => { @@ -5599,6 +5608,10 @@ impl fmt::Display for Statement { write!(f, " DEFAULT COLLATE {collate}")?; } + if let Some(with) = with { + write!(f, " WITH ({})", display_comma_separated(with))?; + } + if let Some(options) = options { write!(f, " OPTIONS({})", display_comma_separated(options))?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9cef22ed..b582e793 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4862,6 +4862,12 @@ impl<'a> Parser<'a> { None }; + let with = if self.peek_keyword(Keyword::WITH) { + Some(self.parse_options(Keyword::WITH)?) + } else { + None + }; + let options = if self.peek_keyword(Keyword::OPTIONS) { Some(self.parse_options(Keyword::OPTIONS)?) } else { @@ -4871,6 +4877,7 @@ impl<'a> Parser<'a> { Ok(Statement::CreateSchema { schema_name, if_not_exists, + with, options, default_collate_spec, }) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 399fdb3d..abcadb45 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4268,6 +4268,9 @@ fn parse_create_schema() { verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a OPTIONS(key1 = 'value1')"#); verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a OPTIONS()"#); verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a DEFAULT COLLATE 'und:ci' OPTIONS()"#); + verified_stmt(r#"CREATE SCHEMA a.b.c WITH (key1 = 'value1', key2 = 'value2')"#); + verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a WITH (key1 = 'value1')"#); + verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a WITH ()"#); } #[test] From 559b7e36d9bafbc6da9e3972136ccfae5c22aeb8 Mon Sep 17 00:00:00 2001 From: vimko <1399950+vimko@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:26:07 +0800 Subject: [PATCH 085/130] Add support for `ALTER TABLE DROP INDEX` (#1865) --- src/ast/ddl.rs | 7 +++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 3 +++ tests/sqlparser_mysql.rs | 10 ++++++++++ 4 files changed, 21 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index b0a3708c..059c6196 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -187,6 +187,12 @@ pub enum AlterTableOperation { DropForeignKey { name: Ident, }, + /// `DROP INDEX ` + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + DropIndex { + name: Ident, + }, /// `ENABLE ALWAYS RULE rewrite_rule_name` /// /// Note: this is a PostgreSQL-specific operation. @@ -606,6 +612,7 @@ impl fmt::Display for AlterTableOperation { } AlterTableOperation::DropPrimaryKey => write!(f, "DROP PRIMARY KEY"), AlterTableOperation::DropForeignKey { name } => write!(f, "DROP FOREIGN KEY {name}"), + AlterTableOperation::DropIndex { name } => write!(f, "DROP INDEX {name}"), AlterTableOperation::DropColumn { has_column_keyword, column_name, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 39d30df9..14664b4c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1115,6 +1115,7 @@ impl Spanned for AlterTableOperation { .union_opt(&with_name.as_ref().map(|n| n.span)), AlterTableOperation::DropPrimaryKey => Span::empty(), AlterTableOperation::DropForeignKey { name } => name.span, + AlterTableOperation::DropIndex { name } => name.span, AlterTableOperation::EnableAlwaysRule { name } => name.span, AlterTableOperation::EnableAlwaysTrigger { name } => name.span, AlterTableOperation::EnableReplicaRule { name } => name.span, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b582e793..6831d52e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8636,6 +8636,9 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::KEY]) { let name = self.parse_identifier()?; AlterTableOperation::DropForeignKey { name } + } else if self.parse_keyword(Keyword::INDEX) { + let name = self.parse_identifier()?; + AlterTableOperation::DropIndex { name } } else if self.parse_keyword(Keyword::PROJECTION) && dialect_of!(self is ClickHouseDialect|GenericDialect) { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b1b7d539..540348ff 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4025,3 +4025,13 @@ fn parse_drop_index() { _ => unreachable!(), } } + +#[test] +fn parse_alter_table_drop_index() { + assert_matches!( + alter_table_op( + mysql_and_generic().verified_stmt("ALTER TABLE tab DROP INDEX idx_index") + ), + AlterTableOperation::DropIndex { name } if name.value == "idx_index" + ); +} From 9fc9009b94c67f0b3314b7ca2b6b0a5f808c6027 Mon Sep 17 00:00:00 2001 From: Jacob Wujciak-Jens Date: Wed, 11 Jun 2025 01:54:21 +0200 Subject: [PATCH 086/130] chore: Replace archived actions-rs/install action (#1876) --- .github/workflows/rust.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b5744e86..3abf9d38 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,6 +19,9 @@ name: Rust on: [push, pull_request] +permissions: + contents: read + jobs: codestyle: @@ -85,11 +88,8 @@ jobs: uses: ./.github/actions/setup-builder with: rust-version: ${{ matrix.rust }} + - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Install Tarpaulin - uses: actions-rs/install@v0.1 - with: - crate: cargo-tarpaulin - version: 0.14.2 - use-tool-cache: true + run: cargo install cargo-tarpaulin - name: Test run: cargo test --all-features From c2e83d49f6442bd93faa1abd011d4fac170bec7f Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Wed, 11 Jun 2025 18:11:07 +0200 Subject: [PATCH 087/130] Allow `IF NOT EXISTS` after table name for Snowflake (#1881) --- src/dialect/snowflake.rs | 3 +++ tests/sqlparser_snowflake.rs | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ccce1619..990e2ea2 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -560,6 +560,9 @@ pub fn parse_create_table( builder.storage_serialization_policy = Some(parse_storage_serialization_policy(parser)?); } + Keyword::IF if parser.parse_keywords(&[Keyword::NOT, Keyword::EXISTS]) => { + builder = builder.if_not_exists(true); + } _ => { return parser.expected("end of statement", next_token); } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b4d62506..7a164c24 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -446,6 +446,27 @@ fn test_snowflake_create_table_if_not_exists() { } _ => unreachable!(), } + + for (sql, parse_to) in [ + ( + r#"CREATE TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + r#"CREATE TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + ), + ( + r#"CREATE TABLE "A"."B"."C" IF NOT EXISTS (v VARIANT)"#, + r#"CREATE TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + ), + ( + r#"CREATE TRANSIENT TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + r#"CREATE TRANSIENT TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + ), + ( + r#"CREATE TRANSIENT TABLE "A"."B"."C" IF NOT EXISTS (v VARIANT)"#, + r#"CREATE TRANSIENT TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + ), + ] { + snowflake().one_statement_parses_to(sql, parse_to); + } } #[test] From 703ba2cf4867c3c7c768836fdacb0193eb5d0bca Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Wed, 11 Jun 2025 18:12:30 +0200 Subject: [PATCH 088/130] Support `DISTINCT AS { STRUCT | VALUE }` for BigQuery (#1880) --- src/ast/query.rs | 10 ++++++++-- src/parser/mod.rs | 39 +++++++++++++++++++++++++------------ tests/sqlparser_bigquery.rs | 38 ++++++++++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 4398531c..1fb93b6c 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -3338,15 +3338,19 @@ impl fmt::Display for OpenJsonTableColumn { } /// BigQuery supports ValueTables which have 2 modes: -/// `SELECT AS STRUCT` -/// `SELECT AS VALUE` +/// `SELECT [ALL | DISTINCT] AS STRUCT` +/// `SELECT [ALL | DISTINCT] AS VALUE` +/// /// +/// #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ValueTableMode { AsStruct, AsValue, + DistinctAsStruct, + DistinctAsValue, } impl fmt::Display for ValueTableMode { @@ -3354,6 +3358,8 @@ impl fmt::Display for ValueTableMode { match self { ValueTableMode::AsStruct => write!(f, "AS STRUCT"), ValueTableMode::AsValue => write!(f, "AS VALUE"), + ValueTableMode::DistinctAsStruct => write!(f, "DISTINCT AS STRUCT"), + ValueTableMode::DistinctAsValue => write!(f, "DISTINCT AS VALUE"), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6831d52e..2c208e2e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11505,18 +11505,7 @@ impl<'a> Parser<'a> { } let select_token = self.expect_keyword(Keyword::SELECT)?; - let value_table_mode = - if dialect_of!(self is BigQueryDialect) && self.parse_keyword(Keyword::AS) { - if self.parse_keyword(Keyword::VALUE) { - Some(ValueTableMode::AsValue) - } else if self.parse_keyword(Keyword::STRUCT) { - Some(ValueTableMode::AsStruct) - } else { - self.expected("VALUE or STRUCT", self.peek_token())? - } - } else { - None - }; + let value_table_mode = self.parse_value_table_mode()?; let mut top_before_distinct = false; let mut top = None; @@ -11692,6 +11681,32 @@ impl<'a> Parser<'a> { }) } + fn parse_value_table_mode(&mut self) -> Result, ParserError> { + if !dialect_of!(self is BigQueryDialect) { + return Ok(None); + } + + let mode = if self.parse_keywords(&[Keyword::DISTINCT, Keyword::AS, Keyword::VALUE]) { + Some(ValueTableMode::DistinctAsValue) + } else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::AS, Keyword::STRUCT]) { + Some(ValueTableMode::DistinctAsStruct) + } else if self.parse_keywords(&[Keyword::AS, Keyword::VALUE]) + || self.parse_keywords(&[Keyword::ALL, Keyword::AS, Keyword::VALUE]) + { + Some(ValueTableMode::AsValue) + } else if self.parse_keywords(&[Keyword::AS, Keyword::STRUCT]) + || self.parse_keywords(&[Keyword::ALL, Keyword::AS, Keyword::STRUCT]) + { + Some(ValueTableMode::AsStruct) + } else if self.parse_keyword(Keyword::AS) { + self.expected("VALUE or STRUCT", self.peek_token())? + } else { + None + }; + + Ok(mode) + } + /// Invoke `f` after first setting the parser's `ParserState` to `state`. /// /// Upon return, restores the parser's state to what it started at. diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 8f54f3c9..b64f190f 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2313,16 +2313,46 @@ fn bigquery_select_expr_star() { #[test] fn test_select_as_struct() { - bigquery().verified_only_select("SELECT * FROM (SELECT AS VALUE STRUCT(123 AS a, false AS b))"); + for (sql, parse_to) in [ + ( + "SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))", + "SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))", + ), + ( + "SELECT * FROM (SELECT DISTINCT AS STRUCT STRUCT(123 AS a, false AS b))", + "SELECT * FROM (SELECT DISTINCT AS STRUCT STRUCT(123 AS a, false AS b))", + ), + ( + "SELECT * FROM (SELECT ALL AS STRUCT STRUCT(123 AS a, false AS b))", + "SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))", + ), + ] { + bigquery().one_statement_parses_to(sql, parse_to); + } + let select = bigquery().verified_only_select("SELECT AS STRUCT 1 AS a, 2 AS b"); assert_eq!(Some(ValueTableMode::AsStruct), select.value_table_mode); } #[test] fn test_select_as_value() { - bigquery().verified_only_select( - "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", - ); + for (sql, parse_to) in [ + ( + "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + ), + ( + "SELECT * FROM (SELECT DISTINCT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + "SELECT * FROM (SELECT DISTINCT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + ), + ( + "SELECT * FROM (SELECT ALL AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + ), + ] { + bigquery().one_statement_parses_to(sql, parse_to); + } + let select = bigquery().verified_only_select("SELECT AS VALUE STRUCT(1 AS a, 2 AS b) AS xyz"); assert_eq!(Some(ValueTableMode::AsValue), select.value_table_mode); } From 0f2208d293d4eb8c9ca7502881377609aaf0a5dc Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 14 Jun 2025 00:45:34 -0400 Subject: [PATCH 089/130] Prepare for `0.57.0` release (#1885) --- Cargo.toml | 2 +- changelog/0.57.0.md | 95 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 changelog/0.57.0.md diff --git a/Cargo.toml b/Cargo.toml index d746775e..07e44f66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.56.0" +version = "0.57.0" authors = ["Apache DataFusion "] homepage = "https://github.com/apache/datafusion-sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/changelog/0.57.0.md b/changelog/0.57.0.md new file mode 100644 index 00000000..200bb73a --- /dev/null +++ b/changelog/0.57.0.md @@ -0,0 +1,95 @@ + + +# sqlparser-rs 0.57.0 Changelog + +This release consists of 39 commits from 19 contributors. See credits at the end of this changelog for more information. + +**Implemented enhancements:** + +- feat: Hive: support `SORT BY` direction [#1873](https://github.com/apache/datafusion-sqlparser-rs/pull/1873) (chenkovsky) + +**Other:** + +- Support some of pipe operators [#1759](https://github.com/apache/datafusion-sqlparser-rs/pull/1759) (simonvandel) +- Added support for `DROP DOMAIN` [#1828](https://github.com/apache/datafusion-sqlparser-rs/pull/1828) (LucaCappelletti94) +- Improve support for cursors for SQL Server [#1831](https://github.com/apache/datafusion-sqlparser-rs/pull/1831) (aharpervc) +- Add all missing table options to be handled in any order [#1747](https://github.com/apache/datafusion-sqlparser-rs/pull/1747) (benrsatori) +- Add `CREATE TRIGGER` support for SQL Server [#1810](https://github.com/apache/datafusion-sqlparser-rs/pull/1810) (aharpervc) +- Added support for `CREATE DOMAIN` [#1830](https://github.com/apache/datafusion-sqlparser-rs/pull/1830) (LucaCappelletti94) +- Allow stored procedures to be defined without `BEGIN`/`END` [#1834](https://github.com/apache/datafusion-sqlparser-rs/pull/1834) (aharpervc) +- Add support for the MATCH and REGEXP binary operators [#1840](https://github.com/apache/datafusion-sqlparser-rs/pull/1840) (lovasoa) +- Fix: parsing ident starting with underscore in certain dialects [#1835](https://github.com/apache/datafusion-sqlparser-rs/pull/1835) (MohamedAbdeen21) +- implement pretty-printing with `{:#}` [#1847](https://github.com/apache/datafusion-sqlparser-rs/pull/1847) (lovasoa) +- Fix big performance issue in string serialization [#1848](https://github.com/apache/datafusion-sqlparser-rs/pull/1848) (lovasoa) +- Add support for `DENY` statements [#1836](https://github.com/apache/datafusion-sqlparser-rs/pull/1836) (aharpervc) +- Postgresql: Add `REPLICA IDENTITY` operation for `ALTER TABLE` [#1844](https://github.com/apache/datafusion-sqlparser-rs/pull/1844) (MohamedAbdeen21) +- Add support for INCLUDE/EXCLUDE NULLS for UNPIVOT [#1849](https://github.com/apache/datafusion-sqlparser-rs/pull/1849) (Vedin) +- pretty print improvements [#1851](https://github.com/apache/datafusion-sqlparser-rs/pull/1851) (lovasoa) +- fix new rust 1.87 cargo clippy warnings [#1856](https://github.com/apache/datafusion-sqlparser-rs/pull/1856) (lovasoa) +- Update criterion requirement from 0.5 to 0.6 in /sqlparser_bench [#1857](https://github.com/apache/datafusion-sqlparser-rs/pull/1857) (dependabot[bot]) +- pretty-print CREATE TABLE statements [#1854](https://github.com/apache/datafusion-sqlparser-rs/pull/1854) (lovasoa) +- pretty-print CREATE VIEW statements [#1855](https://github.com/apache/datafusion-sqlparser-rs/pull/1855) (lovasoa) +- Handle optional datatypes properly in `CREATE FUNCTION` statements [#1826](https://github.com/apache/datafusion-sqlparser-rs/pull/1826) (LucaCappelletti94) +- Mysql: Add `SRID` column option [#1852](https://github.com/apache/datafusion-sqlparser-rs/pull/1852) (MohamedAbdeen21) +- Add support for table valued functions for SQL Server [#1839](https://github.com/apache/datafusion-sqlparser-rs/pull/1839) (aharpervc) +- Keep the COLUMN keyword only if it exists when dropping the column [#1862](https://github.com/apache/datafusion-sqlparser-rs/pull/1862) (git-hulk) +- Add support for parameter default values in SQL Server [#1866](https://github.com/apache/datafusion-sqlparser-rs/pull/1866) (aharpervc) +- Add support for `TABLESAMPLE` pipe operator [#1860](https://github.com/apache/datafusion-sqlparser-rs/pull/1860) (hendrikmakait) +- Adds support for mysql's drop index [#1864](https://github.com/apache/datafusion-sqlparser-rs/pull/1864) (dmzmk) +- Fix: GROUPING SETS accept values without parenthesis [#1867](https://github.com/apache/datafusion-sqlparser-rs/pull/1867) (Vedin) +- Add ICEBERG keyword support to ALTER TABLE statement [#1869](https://github.com/apache/datafusion-sqlparser-rs/pull/1869) (osipovartem) +- MySQL: Support `index_name` in FK constraints [#1871](https://github.com/apache/datafusion-sqlparser-rs/pull/1871) (MohamedAbdeen21) +- Postgres: Apply `ONLY` keyword per table in TRUNCATE stmt [#1872](https://github.com/apache/datafusion-sqlparser-rs/pull/1872) (MohamedAbdeen21) +- Fix `CASE` expression spans [#1874](https://github.com/apache/datafusion-sqlparser-rs/pull/1874) (eliaperantoni) +- MySQL: `[[NOT] ENFORCED]` in CHECK constraint [#1870](https://github.com/apache/datafusion-sqlparser-rs/pull/1870) (MohamedAbdeen21) +- Add support for `CREATE SCHEMA WITH ( )` [#1877](https://github.com/apache/datafusion-sqlparser-rs/pull/1877) (utay) +- Add support for `ALTER TABLE DROP INDEX` [#1865](https://github.com/apache/datafusion-sqlparser-rs/pull/1865) (vimko) +- chore: Replace archived actions-rs/install action [#1876](https://github.com/apache/datafusion-sqlparser-rs/pull/1876) (assignUser) +- Allow `IF NOT EXISTS` after table name for Snowflake [#1881](https://github.com/apache/datafusion-sqlparser-rs/pull/1881) (bombsimon) +- Support `DISTINCT AS { STRUCT | VALUE }` for BigQuery [#1880](https://github.com/apache/datafusion-sqlparser-rs/pull/1880) (bombsimon) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 7 Ophir LOJKINE + 6 Andrew Harper + 6 Mohamed Abdeen + 3 Luca Cappelletti + 2 Denys Tsomenko + 2 Simon Sawert + 1 Andrew Lamb + 1 Artem Osipov + 1 Chen Chongchen + 1 Dmitriy Mazurin + 1 Elia Perantoni + 1 Hendrik Makait + 1 Jacob Wujciak-Jens + 1 Simon Vandel Sillesen + 1 Yannick Utard + 1 benrsatori + 1 dependabot[bot] + 1 hulk + 1 vimko +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. + From e406422bacb6a8d1a09f053faaa1f9a063dc0f40 Mon Sep 17 00:00:00 2001 From: Artem Osipov <59066880+osipovartem@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:33:16 +0300 Subject: [PATCH 090/130] Add support for cluster by expressions (#1883) Co-authored-by: osipovartem --- src/ast/dml.rs | 4 +++- src/ast/helpers/stmt_create_table.rs | 6 +++--- src/dialect/snowflake.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_bigquery.rs | 4 ++-- tests/sqlparser_snowflake.rs | 26 +++++++++++++++++++++----- 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index da82a4ed..292650c8 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -175,7 +175,9 @@ pub struct CreateTable { pub partition_by: Option>, /// BigQuery: Table clustering column list. /// - pub cluster_by: Option>>, + /// Snowflake: Table clustering list which contains base column, expressions on base columns. + /// + pub cluster_by: Option>>, /// Hive: Table clustering column list. /// pub clustered_by: Option, diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 542d30ea..d66a869b 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -90,7 +90,7 @@ pub struct CreateTableBuilder { pub primary_key: Option>, pub order_by: Option>, pub partition_by: Option>, - pub cluster_by: Option>>, + pub cluster_by: Option>>, pub clustered_by: Option, pub inherits: Option>, pub strict: bool, @@ -279,7 +279,7 @@ impl CreateTableBuilder { self } - pub fn cluster_by(mut self, cluster_by: Option>>) -> Self { + pub fn cluster_by(mut self, cluster_by: Option>>) -> Self { self.cluster_by = cluster_by; self } @@ -542,7 +542,7 @@ impl TryFrom for CreateTableBuilder { #[derive(Default)] pub(crate) struct CreateTableConfiguration { pub partition_by: Option>, - pub cluster_by: Option>>, + pub cluster_by: Option>>, pub inherits: Option>, pub table_options: CreateTableOptions, } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 990e2ea2..ea0b94a6 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -453,7 +453,7 @@ pub fn parse_create_table( parser.expect_keyword_is(Keyword::BY)?; parser.expect_token(&Token::LParen)?; let cluster_by = Some(WrappedCollection::Parentheses( - parser.parse_comma_separated(|p| p.parse_identifier())?, + parser.parse_comma_separated(|p| p.parse_expr())?, )); parser.expect_token(&Token::RParen)?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2c208e2e..73cc3e0e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7316,7 +7316,7 @@ impl<'a> Parser<'a> { if dialect_of!(self is BigQueryDialect | GenericDialect) { if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { cluster_by = Some(WrappedCollection::NoWrapping( - self.parse_comma_separated(|p| p.parse_identifier())?, + self.parse_comma_separated(|p| p.parse_expr())?, )); }; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index b64f190f..6a303577 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -536,8 +536,8 @@ fn parse_create_table_with_options() { ( Some(Box::new(Expr::Identifier(Ident::new("_PARTITIONDATE")))), Some(WrappedCollection::NoWrapping(vec![ - Ident::new("userid"), - Ident::new("age"), + Expr::Identifier(Ident::new("userid")), + Expr::Identifier(Ident::new("age")), ])), CreateTableOptions::Options(vec![ SqlOption::KeyValue { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7a164c24..7d734ca2 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -471,15 +471,31 @@ fn test_snowflake_create_table_if_not_exists() { #[test] fn test_snowflake_create_table_cluster_by() { - match snowflake().verified_stmt("CREATE TABLE my_table (a INT) CLUSTER BY (a, b)") { + match snowflake().verified_stmt("CREATE TABLE my_table (a INT) CLUSTER BY (a, b, my_func(c))") { Statement::CreateTable(CreateTable { name, cluster_by, .. }) => { assert_eq!("my_table", name.to_string()); assert_eq!( Some(WrappedCollection::Parentheses(vec![ - Ident::new("a"), - Ident::new("b"), + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")), + Expr::Function(Function { + name: ObjectName::from(vec![Ident::new("my_func")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("c")) + ))], + duplicate_treatment: None, + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }), ])), cluster_by ) @@ -903,8 +919,8 @@ fn test_snowflake_create_iceberg_table_all_options() { assert_eq!("my_table", name.to_string()); assert_eq!( Some(WrappedCollection::Parentheses(vec![ - Ident::new("a"), - Ident::new("b"), + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")), ])), cluster_by ); From 6f423969b0e723eeded2e5b7c0587aefae91ec54 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 16 Jun 2025 12:37:18 -0400 Subject: [PATCH 091/130] Add license header to display_utils.rs and pretty_print.rs (#1887) --- src/display_utils.rs | 17 +++++++++++++++++ tests/pretty_print.rs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/display_utils.rs b/src/display_utils.rs index e594a34e..ba36fccd 100644 --- a/src/display_utils.rs +++ b/src/display_utils.rs @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + //! Utilities for formatting SQL AST nodes with pretty printing support. //! //! The module provides formatters that implement the `Display` trait with support diff --git a/tests/pretty_print.rs b/tests/pretty_print.rs index e1d35eb0..f5a9d861 100644 --- a/tests/pretty_print.rs +++ b/tests/pretty_print.rs @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + use sqlparser::dialect::GenericDialect; use sqlparser::parser::Parser; From be30697efb94cecdca749cc540c5a662c63fad3f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 17 Jun 2025 10:46:58 -0400 Subject: [PATCH 092/130] Add license header check to CI (#1888) --- .github/workflows/license.yml | 39 +++++++++++++++++++++++++++++++ dev/release/rat_exclude_files.txt | 9 +++---- 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/license.yml diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml new file mode 100644 index 00000000..c851bff3 --- /dev/null +++ b/.github/workflows/license.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: license + +# trigger for all PRs and changes to main +on: + push: + branches: + - main + pull_request: + +jobs: + + rat: + name: Release Audit Tool (RAT) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.8 + - name: Audit licenses + run: ./dev/release/run-rat.sh . diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index 562eec2f..280b1bce 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -1,7 +1,8 @@ -# Files to exclude from the Apache Rat (license) check -.gitignore .tool-versions +target/* +**.gitignore +rat.txt dev/release/rat_exclude_files.txt -fuzz/.gitignore sqlparser_bench/img/flamegraph.svg - +**Cargo.lock +filtered_rat.txt From b1b379e57086ad7fcfa67136c0c64b14d4efab9d Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 18 Jun 2025 13:00:53 +0800 Subject: [PATCH 093/130] Add support of parsing struct field's options in BigQuery (#1890) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 12 ++- src/parser/mod.rs | 3 + tests/sqlparser_bigquery.rs | 159 ++++++++++++++++++++++++---------- tests/sqlparser_clickhouse.rs | 10 ++- tests/sqlparser_duckdb.rs | 6 ++ 5 files changed, 138 insertions(+), 52 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a1d8ff6f..04401a48 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -428,14 +428,22 @@ impl fmt::Display for Interval { pub struct StructField { pub field_name: Option, pub field_type: DataType, + /// Struct field options. + /// See [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#column_name_and_column_schema) + pub options: Option>, } impl fmt::Display for StructField { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(name) = &self.field_name { - write!(f, "{name} {}", self.field_type) + write!(f, "{name} {}", self.field_type)?; } else { - write!(f, "{}", self.field_type) + write!(f, "{}", self.field_type)?; + } + if let Some(options) = &self.options { + write!(f, " OPTIONS({})", display_separated(options, ", ")) + } else { + Ok(()) } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 73cc3e0e..19c11d29 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3076,6 +3076,7 @@ impl<'a> Parser<'a> { Ok(StructField { field_name: Some(field_name), field_type, + options: None, }) }); self.expect_token(&Token::RParen)?; @@ -3109,10 +3110,12 @@ impl<'a> Parser<'a> { let (field_type, trailing_bracket) = self.parse_data_type_helper()?; + let options = self.maybe_parse_options(Keyword::OPTIONS)?; Ok(( StructField { field_name, field_type, + options, }, trailing_bracket, )) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 6a303577..e37e4a44 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -601,11 +601,13 @@ fn parse_nested_data_types() { field_name: Some("a".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket( Box::new(DataType::Int64,) - )) + )), + options: None, }, StructField { field_name: Some("b".into()), - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }, ], StructBracketKind::AngleBrackets @@ -619,6 +621,7 @@ fn parse_nested_data_types() { vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }], StructBracketKind::AngleBrackets ), @@ -771,6 +774,7 @@ fn parse_typed_struct_syntax_bigquery() { fields: vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -799,7 +803,8 @@ fn parse_typed_struct_syntax_bigquery() { quote_style: None, span: Span::empty(), }), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident { @@ -807,7 +812,8 @@ fn parse_typed_struct_syntax_bigquery() { quote_style: None, span: Span::empty(), }), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }, ] }, @@ -825,17 +831,20 @@ fn parse_typed_struct_syntax_bigquery() { field_name: Some("arr".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Float64 - ))) + ))), + options: None, }, StructField { field_name: Some("str".into()), field_type: DataType::Struct( vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }], StructBracketKind::AngleBrackets - ) + ), + options: None, }, ] }, @@ -858,13 +867,15 @@ fn parse_typed_struct_syntax_bigquery() { field_type: DataType::Struct( Default::default(), StructBracketKind::AngleBrackets - ) + ), + options: None, }, StructField { field_name: Some("y".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Struct(Default::default(), StructBracketKind::AngleBrackets) - ))) + ))), + options: None, }, ] }, @@ -879,7 +890,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::Value(Value::Boolean(true).with_empty_span())], fields: vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -891,7 +903,8 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -907,7 +920,8 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Date + field_type: DataType::Date, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -920,7 +934,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Datetime(None) + field_type: DataType::Datetime(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -930,7 +945,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::value(number("5.0"))], fields: vec![StructField { field_name: None, - field_type: DataType::Float64 + field_type: DataType::Float64, + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -940,7 +956,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::value(number("1"))], fields: vec![StructField { field_name: None, - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[3]) @@ -962,7 +979,8 @@ fn parse_typed_struct_syntax_bigquery() { })], fields: vec![StructField { field_name: None, - field_type: DataType::Interval + field_type: DataType::Interval, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -977,7 +995,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::JSON + field_type: DataType::JSON, + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -993,7 +1012,8 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::String(Some(42)) + field_type: DataType::String(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1006,7 +1026,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Timestamp(None, TimezoneInfo::None) + field_type: DataType::Timestamp(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1020,7 +1041,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Time(None, TimezoneInfo::None) + field_type: DataType::Time(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1037,7 +1059,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Numeric(ExactNumberInfo::None) + field_type: DataType::Numeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1050,7 +1073,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::BigNumeric(ExactNumberInfo::None) + field_type: DataType::BigNumeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1067,10 +1091,12 @@ fn parse_typed_struct_syntax_bigquery() { StructField { field_name: Some("key".into()), field_type: DataType::Int64, + options: None, }, StructField { field_name: Some("value".into()), field_type: DataType::Int64, + options: None, }, ] }, @@ -1092,6 +1118,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { fields: vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1120,7 +1147,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { quote_style: None, span: Span::empty(), }), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident { @@ -1128,7 +1156,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { quote_style: None, span: Span::empty(), }), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }, ] }, @@ -1151,13 +1180,15 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { field_type: DataType::Struct( Default::default(), StructBracketKind::AngleBrackets - ) + ), + options: None, }, StructField { field_name: Some("y".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Struct(Default::default(), StructBracketKind::AngleBrackets) - ))) + ))), + options: None, }, ] }, @@ -1172,7 +1203,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::Value(Value::Boolean(true).with_empty_span())], fields: vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1184,7 +1216,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1200,7 +1233,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Date + field_type: DataType::Date, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1213,7 +1247,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Datetime(None) + field_type: DataType::Datetime(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1223,7 +1258,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::value(number("5.0"))], fields: vec![StructField { field_name: None, - field_type: DataType::Float64 + field_type: DataType::Float64, + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1233,7 +1269,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::value(number("1"))], fields: vec![StructField { field_name: None, - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[3]) @@ -1255,7 +1292,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { })], fields: vec![StructField { field_name: None, - field_type: DataType::Interval + field_type: DataType::Interval, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1270,7 +1308,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::JSON + field_type: DataType::JSON, + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1286,7 +1325,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::String(Some(42)) + field_type: DataType::String(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1299,7 +1339,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Timestamp(None, TimezoneInfo::None) + field_type: DataType::Timestamp(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1313,7 +1354,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Time(None, TimezoneInfo::None) + field_type: DataType::Time(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1330,7 +1372,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Numeric(ExactNumberInfo::None) + field_type: DataType::Numeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1343,7 +1386,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::BigNumeric(ExactNumberInfo::None) + field_type: DataType::BigNumeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1360,7 +1404,8 @@ fn parse_typed_struct_with_field_name_bigquery() { values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1372,7 +1417,8 @@ fn parse_typed_struct_with_field_name_bigquery() { )], fields: vec![StructField { field_name: Some(Ident::from("y")), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1387,11 +1433,13 @@ fn parse_typed_struct_with_field_name_bigquery() { fields: vec![ StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident::from("y")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, } ] }, @@ -1409,7 +1457,8 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1421,7 +1470,8 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { )], fields: vec![StructField { field_name: Some(Ident::from("y")), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1436,11 +1486,13 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { fields: vec![ StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident::from("y")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, } ] }, @@ -2407,3 +2459,16 @@ fn test_any_type() { fn test_any_type_dont_break_custom_type() { bigquery_and_generic().verified_stmt("CREATE TABLE foo (x ANY)"); } + +#[test] +fn test_struct_field_options() { + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRUCT, ", + "f1 STRUCT<", + "a STRING OPTIONS(description = 'This is a string', type = 'string'), ", + "b INT64", + "> OPTIONS(description = 'This is a struct field')", + ")", + )); +} diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index d0218b6c..93b4c4f5 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -669,11 +669,13 @@ fn parse_create_table_with_nested_data_types() { DataType::Tuple(vec![ StructField { field_name: None, - field_type: DataType::FixedString(128) + field_type: DataType::FixedString(128), + options: None, }, StructField { field_name: None, - field_type: DataType::Int128 + field_type: DataType::Int128, + options: None, } ]) ))), @@ -685,12 +687,14 @@ fn parse_create_table_with_nested_data_types() { StructField { field_name: Some("a".into()), field_type: DataType::Datetime64(9, None), + options: None, }, StructField { field_name: Some("b".into()), field_type: DataType::Array(ArrayElemTypeDef::Parenthesis( Box::new(DataType::Uuid) - )) + )), + options: None, }, ]), options: vec![], diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 8e498365..f503ed55 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -44,10 +44,12 @@ fn test_struct() { StructField { field_name: Some(Ident::new("v")), field_type: DataType::Varchar(None), + options: None, }, StructField { field_name: Some(Ident::new("i")), field_type: DataType::Integer(None), + options: None, }, ], StructBracketKind::Parentheses, @@ -84,6 +86,7 @@ fn test_struct() { StructField { field_name: Some(Ident::new("v")), field_type: DataType::Varchar(None), + options: None, }, StructField { field_name: Some(Ident::new("s")), @@ -92,14 +95,17 @@ fn test_struct() { StructField { field_name: Some(Ident::new("a1")), field_type: DataType::Integer(None), + options: None, }, StructField { field_name: Some(Ident::new("a2")), field_type: DataType::Varchar(None), + options: None, }, ], StructBracketKind::Parentheses, ), + options: None, }, ], StructBracketKind::Parentheses, From 185a4902189565031fa066d165594325172f2893 Mon Sep 17 00:00:00 2001 From: hulk Date: Fri, 20 Jun 2025 22:56:26 +0800 Subject: [PATCH 094/130] Fix parsing error when having fields after nested struct in BigQuery (#1897) --- src/parser/mod.rs | 11 +--- tests/sqlparser_bigquery.rs | 104 ++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 38 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 19c11d29..ece9ba65 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3034,7 +3034,6 @@ impl<'a> Parser<'a> { where F: FnMut(&mut Parser<'a>) -> Result<(StructField, MatchedTrailingBracket), ParserError>, { - let start_token = self.peek_token(); self.expect_keyword_is(Keyword::STRUCT)?; // Nothing to do if we have no type information. @@ -3047,16 +3046,10 @@ impl<'a> Parser<'a> { let trailing_bracket = loop { let (def, trailing_bracket) = elem_parser(self)?; field_defs.push(def); - if !self.consume_token(&Token::Comma) { + // The struct field definition is finished if it occurs `>>` or comma. + if trailing_bracket.0 || !self.consume_token(&Token::Comma) { break trailing_bracket; } - - // Angle brackets are balanced so we only expect the trailing `>>` after - // we've matched all field types for the current struct. - // e.g. this is invalid syntax `STRUCT>>, INT>(NULL)` - if trailing_bracket.0 { - return parser_err!("unmatched > in STRUCT definition", start_token.span.start); - } }; Ok(( diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index e37e4a44..810d8480 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -635,35 +635,6 @@ fn parse_nested_data_types() { } } -#[test] -fn parse_invalid_brackets() { - let sql = "SELECT STRUCT>(NULL)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError("unmatched > in STRUCT literal".to_string()) - ); - - let sql = "SELECT STRUCT>>(NULL)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError("Expected: (, found: >".to_string()) - ); - - let sql = "CREATE TABLE table (x STRUCT>>)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError( - "Expected: ',' or ')' after column definition, found: >".to_string() - ) - ); -} - #[test] fn parse_tuple_struct_literal() { // tuple syntax: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#tuple_syntax @@ -2472,3 +2443,78 @@ fn test_struct_field_options() { ")", )); } + +#[test] +fn test_struct_trailing_and_nested_bracket() { + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRING, ", + "f1 STRUCT>, ", + "f2 STRING", + ")", + )); + + // More complex nested structs + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRING, ", + "f1 STRUCT>>, ", + "f2 STRUCT>>>, ", + "f3 STRUCT>", + ")", + )); + + // Bad case with missing closing bracket + assert_eq!( + ParserError::ParserError("Expected: >, found: )".to_owned()), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT after parsing data type STRUCT)".to_owned() + ), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT>)") + .unwrap_err() + ); + + // Base case with redundant closing bracket in nested struct + assert_eq!( + ParserError::ParserError( + "Expected: ',' or ')' after column definition, found: >".to_owned() + ), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT>>, c INT64)") + .unwrap_err() + ); + + let sql = "SELECT STRUCT>(NULL)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError("unmatched > in STRUCT literal".to_string()) + ); + + let sql = "SELECT STRUCT>>(NULL)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError("Expected: (, found: >".to_string()) + ); + + let sql = "CREATE TABLE table (x STRUCT>>)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError( + "Expected: ',' or ')' after column definition, found: >".to_string() + ) + ); +} From 204d3b484db4a659396ae13e7bd2c20ac55fb07c Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Sat, 21 Jun 2025 08:12:07 +0200 Subject: [PATCH 095/130] Extend exception handling (#1884) --- src/ast/mod.rs | 60 +++++++++++++++++++++++++-------- src/dialect/bigquery.rs | 51 +++------------------------- src/dialect/snowflake.rs | 4 +++ src/keywords.rs | 1 + src/parser/mod.rs | 49 +++++++++++++++++++++++++-- tests/sqlparser_bigquery.rs | 9 +++-- tests/sqlparser_common.rs | 7 ++-- tests/sqlparser_snowflake.rs | 64 ++++++++++++++++++++++++++++++++++++ 8 files changed, 178 insertions(+), 67 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 04401a48..369489f5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2990,6 +2990,36 @@ impl From for Statement { } } +/// A representation of a `WHEN` arm with all the identifiers catched and the statements to execute +/// for the arm. +/// +/// Snowflake: +/// BigQuery: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExceptionWhen { + pub idents: Vec, + pub statements: Vec, +} + +impl Display for ExceptionWhen { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "WHEN {idents} THEN", + idents = display_separated(&self.idents, " OR ") + )?; + + if !self.statements.is_empty() { + write!(f, " ")?; + format_statement_list(f, &self.statements)?; + } + + Ok(()) + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -3678,17 +3708,20 @@ pub enum Statement { /// END; /// ``` statements: Vec, - /// Statements of an exception clause. + /// Exception handling with exception clauses. /// Example: /// ```sql - /// BEGIN - /// SELECT 1; - /// EXCEPTION WHEN ERROR THEN - /// SELECT 2; - /// SELECT 3; - /// END; + /// EXCEPTION + /// WHEN EXCEPTION_1 THEN + /// SELECT 2; + /// WHEN EXCEPTION_2 OR EXCEPTION_3 THEN + /// SELECT 3; + /// WHEN OTHER THEN + /// SELECT 4; + /// ``` /// - exception_statements: Option>, + /// + exception: Option>, /// TRUE if the statement has an `END` keyword. has_end_keyword: bool, }, @@ -5533,7 +5566,7 @@ impl fmt::Display for Statement { transaction, modifier, statements, - exception_statements, + exception, has_end_keyword, } => { if *syntax_begin { @@ -5555,11 +5588,10 @@ impl fmt::Display for Statement { write!(f, " ")?; format_statement_list(f, statements)?; } - if let Some(exception_statements) = exception_statements { - write!(f, " EXCEPTION WHEN ERROR THEN")?; - if !exception_statements.is_empty() { - write!(f, " ")?; - format_statement_list(f, exception_statements)?; + if let Some(exception_when) = exception { + write!(f, " EXCEPTION")?; + for when in exception_when { + write!(f, " {when}")?; } } if *has_end_keyword { diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 68ca1390..c2cd507c 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -46,7 +46,11 @@ pub struct BigQueryDialect; impl Dialect for BigQueryDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { - self.maybe_parse_statement(parser) + if parser.parse_keyword(Keyword::BEGIN) { + return Some(parser.parse_begin_exception_end()); + } + + None } /// See @@ -141,48 +145,3 @@ impl Dialect for BigQueryDialect { true } } - -impl BigQueryDialect { - fn maybe_parse_statement(&self, parser: &mut Parser) -> Option> { - if parser.peek_keyword(Keyword::BEGIN) { - return Some(self.parse_begin(parser)); - } - None - } - - /// Parse a `BEGIN` statement. - /// - fn parse_begin(&self, parser: &mut Parser) -> Result { - parser.expect_keyword(Keyword::BEGIN)?; - - let statements = parser.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?; - - let has_exception_when_clause = parser.parse_keywords(&[ - Keyword::EXCEPTION, - Keyword::WHEN, - Keyword::ERROR, - Keyword::THEN, - ]); - let exception_statements = if has_exception_when_clause { - if !parser.peek_keyword(Keyword::END) { - Some(parser.parse_statement_list(&[Keyword::END])?) - } else { - Some(Default::default()) - } - } else { - None - }; - - parser.expect_keyword(Keyword::END)?; - - Ok(Statement::StartTransaction { - begin: true, - statements, - exception_statements, - has_end_keyword: true, - transaction: None, - modifier: None, - modes: Default::default(), - }) - } -} diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ea0b94a6..66e04ac2 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -131,6 +131,10 @@ impl Dialect for SnowflakeDialect { } fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keyword(Keyword::BEGIN) { + return Some(parser.parse_begin_exception_end()); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { diff --git a/src/keywords.rs b/src/keywords.rs index f5c5e567..cb6c7a6e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -646,6 +646,7 @@ define_keywords!( ORDER, ORDINALITY, ORGANIZATION, + OTHER, OUT, OUTER, OUTPUT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ece9ba65..be32093f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15092,7 +15092,7 @@ impl<'a> Parser<'a> { transaction: Some(BeginTransactionKind::Transaction), modifier: None, statements: vec![], - exception_statements: None, + exception: None, has_end_keyword: false, }) } @@ -15124,11 +15124,56 @@ impl<'a> Parser<'a> { transaction, modifier, statements: vec![], - exception_statements: None, + exception: None, has_end_keyword: false, }) } + pub fn parse_begin_exception_end(&mut self) -> Result { + let statements = self.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?; + + let exception = if self.parse_keyword(Keyword::EXCEPTION) { + let mut when = Vec::new(); + + // We can have multiple `WHEN` arms so we consume all cases until `END` + while !self.peek_keyword(Keyword::END) { + self.expect_keyword(Keyword::WHEN)?; + + // Each `WHEN` case can have one or more conditions, e.g. + // WHEN EXCEPTION_1 [OR EXCEPTION_2] THEN + // So we parse identifiers until the `THEN` keyword. + let mut idents = Vec::new(); + + while !self.parse_keyword(Keyword::THEN) { + let ident = self.parse_identifier()?; + idents.push(ident); + + self.maybe_parse(|p| p.expect_keyword(Keyword::OR))?; + } + + let statements = self.parse_statement_list(&[Keyword::WHEN, Keyword::END])?; + + when.push(ExceptionWhen { idents, statements }); + } + + Some(when) + } else { + None + }; + + self.expect_keyword(Keyword::END)?; + + Ok(Statement::StartTransaction { + begin: true, + statements, + exception, + has_end_keyword: true, + transaction: None, + modifier: None, + modes: Default::default(), + }) + } + pub fn parse_end(&mut self) -> Result { let modifier = if !self.dialect.supports_end_transaction_modifier() { None diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 810d8480..0de0b12b 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -261,10 +261,10 @@ fn parse_at_at_identifier() { #[test] fn parse_begin() { - let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; END"#; + let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; RAISE USING MESSAGE = FORMAT('ERR: %s', 'Bad'); END"#; let Statement::StartTransaction { statements, - exception_statements, + exception, has_end_keyword, .. } = bigquery().verified_stmt(sql) @@ -272,7 +272,10 @@ fn parse_begin() { unreachable!(); }; assert_eq!(1, statements.len()); - assert_eq!(1, exception_statements.unwrap().len()); + assert!(exception.is_some()); + + let exception = exception.unwrap(); + assert_eq!(1, exception.len()); assert!(has_end_keyword); bigquery().verified_stmt( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index abcadb45..e4363ff6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8592,8 +8592,11 @@ fn lateral_function() { #[test] fn parse_start_transaction() { let dialects = all_dialects_except(|d| - // BigQuery does not support this syntax - d.is::()); + // BigQuery and Snowflake does not support this syntax + // + // BigQuery: + // Snowflake: + d.is::() || d.is::()); match dialects .verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7d734ca2..b11a2cb0 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4082,3 +4082,67 @@ fn parse_connect_by_root_operator() { "sql parser error: Expected an expression, found: FROM" ); } + +#[test] +fn test_begin_exception_end() { + for sql in [ + "BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END", + "BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE EX_1; END", + "BEGIN SELECT 1; EXCEPTION WHEN FOO THEN SELECT 2; WHEN OTHER THEN SELECT 3; RAISE; END", + "BEGIN BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END; END", + ] { + snowflake().verified_stmt(sql); + } + + let sql = r#" +DECLARE + EXCEPTION_1 EXCEPTION (-20001, 'I caught the expected exception.'); + EXCEPTION_2 EXCEPTION (-20002, 'Not the expected exception!'); + EXCEPTION_3 EXCEPTION (-20003, 'The worst exception...'); +BEGIN + BEGIN + SELECT 1; + EXCEPTION + WHEN EXCEPTION_1 THEN + SELECT 1; + WHEN EXCEPTION_2 OR EXCEPTION_3 THEN + SELECT 2; + SELECT 3; + WHEN OTHER THEN + SELECT 4; + RAISE; + END; +END +"#; + + // Outer `BEGIN` of the two nested `BEGIN` statements. + let Statement::StartTransaction { mut statements, .. } = snowflake() + .parse_sql_statements(sql) + .unwrap() + .pop() + .unwrap() + else { + unreachable!(); + }; + + // Inner `BEGIN` of the two nested `BEGIN` statements. + let Statement::StartTransaction { + statements, + exception, + has_end_keyword, + .. + } = statements.pop().unwrap() + else { + unreachable!(); + }; + + assert_eq!(1, statements.len()); + assert!(has_end_keyword); + + let exception = exception.unwrap(); + assert_eq!(3, exception.len()); + assert_eq!(1, exception[0].idents.len()); + assert_eq!(1, exception[0].statements.len()); + assert_eq!(2, exception[1].idents.len()); + assert_eq!(2, exception[1].statements.len()); +} From 1d0dc7cdd8209d5e68f41675622dd17e2b336e26 Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Sun, 22 Jun 2025 08:02:51 +0100 Subject: [PATCH 096/130] Postgres: Add support for text search types (#1889) --- src/ast/data_type.rs | 10 ++++++++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 6 ++++++ tests/sqlparser_postgres.rs | 31 +++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 3a4958c9..ef04b7e4 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -446,6 +446,14 @@ pub enum DataType { /// /// [PostgreSQL]: https://www.postgresql.org/docs/9.5/functions-geometry.html GeometricType(GeometricTypeKind), + /// PostgreSQL text search vectors, see [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-textsearch.html + TsVector, + /// PostgreSQL text search query, see [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-textsearch.html + TsQuery, } impl fmt::Display for DataType { @@ -738,6 +746,8 @@ impl fmt::Display for DataType { write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) } DataType::GeometricType(kind) => write!(f, "{}", kind), + DataType::TsVector => write!(f, "TSVECTOR"), + DataType::TsQuery => write!(f, "TSQUERY"), } } } diff --git a/src/keywords.rs b/src/keywords.rs index cb6c7a6e..f56178c1 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -935,6 +935,8 @@ define_keywords!( TRY, TRY_CAST, TRY_CONVERT, + TSQUERY, + TSVECTOR, TUPLE, TYPE, UBIGINT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index be32093f..f60323b2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9909,6 +9909,12 @@ impl<'a> Parser<'a> { Ok(DataType::Unsigned) } } + Keyword::TSVECTOR if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { + Ok(DataType::TsVector) + } + Keyword::TSQUERY if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { + Ok(DataType::TsQuery) + } _ => { self.prev_token(); let type_name = self.parse_object_name(false)?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6f0ba9c6..b6605cf1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6201,3 +6201,34 @@ fn parse_alter_table_replica_identity() { _ => unreachable!(), } } + +#[test] +fn parse_ts_datatypes() { + match pg_and_generic().verified_stmt("CREATE TABLE foo (x TSVECTOR)") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "x".into(), + data_type: DataType::TsVector, + options: vec![], + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("CREATE TABLE foo (x TSQUERY)") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "x".into(), + data_type: DataType::TsQuery, + options: vec![], + }] + ); + } + _ => unreachable!(), + } +} From 7865de015f8a45d9545dc7ca0659f37a0c3f7496 Mon Sep 17 00:00:00 2001 From: Dima <111751109+Dimchikkk@users.noreply.github.com> Date: Sun, 22 Jun 2025 08:22:45 +0100 Subject: [PATCH 097/130] Fix `limit` in subqueries (#1899) --- src/ast/mod.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 369489f5..17464f4a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -748,7 +748,7 @@ pub enum Expr { /// `[ NOT ] IN (SELECT ...)` InSubquery { expr: Box, - subquery: Box, + subquery: Box, negated: bool, }, /// `[ NOT ] IN UNNEST(array_expression)` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f60323b2..44bf58d0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3817,7 +3817,7 @@ impl<'a> Parser<'a> { }); } self.expect_token(&Token::LParen)?; - let in_op = match self.maybe_parse(|p| p.parse_query_body(p.dialect.prec_unknown()))? { + let in_op = match self.maybe_parse(|p| p.parse_query())? { Some(subquery) => Expr::InSubquery { expr: Box::new(expr), subquery, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e4363ff6..52054604 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2225,7 +2225,7 @@ fn parse_in_subquery() { assert_eq!( Expr::InSubquery { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: verified_query("SELECT segm FROM bar").body, + subquery: Box::new(verified_query("SELECT segm FROM bar")), negated: false, }, select.selection.unwrap() @@ -2239,7 +2239,9 @@ fn parse_in_union() { assert_eq!( Expr::InSubquery { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: verified_query("(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)").body, + subquery: Box::new(verified_query( + "(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)" + )), negated: false, }, select.selection.unwrap() @@ -15303,6 +15305,11 @@ fn parse_return() { let _ = all_dialects().verified_stmt("RETURN 1"); } +#[test] +fn parse_subquery_limit() { + let _ = all_dialects().verified_stmt("SELECT t1_id, t1_name FROM t1 WHERE t1_id IN (SELECT t2_id FROM t2 WHERE t1_name = t2_name LIMIT 10)"); +} + #[test] fn test_open() { let open_cursor = "OPEN Employee_Cursor"; From 5d63663bc6cd78d63d0cb2581ffc71a1dee3559d Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Mon, 23 Jun 2025 23:18:03 -0700 Subject: [PATCH 098/130] Use `IndexColumn` in all index definitions (#1900) --- src/ast/ddl.rs | 14 ++++---- src/ast/spans.rs | 35 +++++++++++-------- src/parser/mod.rs | 48 +++++++++++++++++-------- src/test_utils.rs | 44 +++++++++++++++++++++++ tests/sqlparser_mysql.rs | 75 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 35 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 059c6196..e35332da 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -32,9 +32,9 @@ use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, CommentDef, CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, - FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, - OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, - ValueWithSpan, + FunctionDeterminismSpecifier, FunctionParallel, Ident, IndexColumn, MySQLColumnPosition, + ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, + Value, ValueWithSpan, }; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -979,7 +979,7 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Identifiers of the columns that are unique. - columns: Vec, + columns: Vec, index_options: Vec, characteristics: Option, /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` @@ -1015,7 +1015,7 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Identifiers of the columns that form the primary key. - columns: Vec, + columns: Vec, index_options: Vec, characteristics: Option, }, @@ -1060,7 +1060,7 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Referred column identifier list. - columns: Vec, + columns: Vec, }, /// MySQLs [fulltext][1] definition. Since the [`SPATIAL`][2] definition is exactly the same, /// and MySQL displays both the same way, it is part of this definition as well. @@ -1083,7 +1083,7 @@ pub enum TableConstraint { /// Optional index name. opt_index_name: Option, /// Referred column identifier list. - columns: Vec, + columns: Vec, }, } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 14664b4c..ca321cc2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -28,16 +28,17 @@ use super::{ ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, - FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, Insert, Interpolate, - InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, NamedWindowDefinition, - ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, - OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, - RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, - SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, - TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, - WhileStatement, WildcardAdditionalOptions, With, WithFill, + FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, + Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, + LateralView, LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, + NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, + OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, + ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, + RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, + SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, + TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, + UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -650,7 +651,7 @@ impl Spanned for TableConstraint { name.iter() .map(|i| i.span) .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), TableConstraint::PrimaryKey { @@ -664,7 +665,7 @@ impl Spanned for TableConstraint { name.iter() .map(|i| i.span) .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), TableConstraint::ForeignKey { @@ -700,7 +701,7 @@ impl Spanned for TableConstraint { } => union_spans( name.iter() .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), + .chain(columns.iter().map(|i| i.span())), ), TableConstraint::FulltextOrSpatial { fulltext: _, @@ -711,7 +712,7 @@ impl Spanned for TableConstraint { opt_index_name .iter() .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), + .chain(columns.iter().map(|i| i.span())), ), } } @@ -745,6 +746,12 @@ impl Spanned for CreateIndex { } } +impl Spanned for IndexColumn { + fn span(&self) -> Span { + self.column.span() + } +} + impl Spanned for CaseStatement { fn span(&self) -> Span { let CaseStatement { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 44bf58d0..35c05dfb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6868,9 +6868,7 @@ impl<'a> Parser<'a> { None }; - self.expect_token(&Token::LParen)?; - let columns = self.parse_comma_separated(Parser::parse_create_index_expr)?; - self.expect_token(&Token::RParen)?; + let columns = self.parse_parenthesized_index_column_list()?; let include = if self.parse_keyword(Keyword::INCLUDE) { self.expect_token(&Token::LParen)?; @@ -8070,7 +8068,7 @@ impl<'a> Parser<'a> { let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::Unique { @@ -8092,7 +8090,7 @@ impl<'a> Parser<'a> { let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::PrimaryKey { @@ -8170,7 +8168,7 @@ impl<'a> Parser<'a> { }; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; Ok(Some(TableConstraint::Index { display_as_key, @@ -8199,7 +8197,7 @@ impl<'a> Parser<'a> { let opt_index_name = self.parse_optional_ident()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; Ok(Some(TableConstraint::FulltextOrSpatial { fulltext, @@ -10601,6 +10599,14 @@ impl<'a> Parser<'a> { self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| p.parse_identifier()) } + /// Parses a parenthesized comma-separated list of index columns, which can be arbitrary + /// expressions with ordering information (and an opclass in some dialects). + fn parse_parenthesized_index_column_list(&mut self) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { + p.parse_create_index_expr() + }) + } + /// Parses a parenthesized comma-separated list of qualified, possibly quoted identifiers. /// For example: `(db1.sc1.tbl1.col1, db1.sc1.tbl1."col 2", ...)` pub fn parse_parenthesized_qualified_column_list( @@ -16527,6 +16533,20 @@ mod tests { }}; } + fn mk_expected_col(name: &str) -> IndexColumn { + IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(name.into()), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None, + } + } + let dialect = TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})]); @@ -16537,7 +16557,7 @@ mod tests { display_as_key: false, name: None, index_type: None, - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16548,7 +16568,7 @@ mod tests { display_as_key: true, name: None, index_type: None, - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16559,7 +16579,7 @@ mod tests { display_as_key: false, name: Some(Ident::with_quote('\'', "index")), index_type: None, - columns: vec![Ident::new("c1"), Ident::new("c2")], + columns: vec![mk_expected_col("c1"), mk_expected_col("c2")], } ); @@ -16570,7 +16590,7 @@ mod tests { display_as_key: false, name: None, index_type: Some(IndexType::BTree), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16581,7 +16601,7 @@ mod tests { display_as_key: false, name: None, index_type: Some(IndexType::Hash), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16592,7 +16612,7 @@ mod tests { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::BTree), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16603,7 +16623,7 @@ mod tests { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::Hash), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); } diff --git a/src/test_utils.rs b/src/test_utils.rs index 24c0ca57..1af9f454 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -448,3 +448,47 @@ pub fn call(function: &str, args: impl IntoIterator) -> Expr { within_group: vec![], }) } + +/// Gets the first index column (mysql calls it a key part) of the first index found in a +/// [`Statement::CreateIndex`], [`Statement::CreateTable`], or [`Statement::AlterTable`]. +pub fn index_column(stmt: Statement) -> Expr { + match stmt { + Statement::CreateIndex(CreateIndex { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + Statement::CreateTable(CreateTable { constraints, .. }) => { + match constraints.first().unwrap() { + TableConstraint::Index { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::Unique { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::PrimaryKey { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + } + } + Statement::AlterTable { operations, .. } => match operations.first().unwrap() { + AlterTableOperation::AddConstraint(TableConstraint::Index { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + AlterTableOperation::AddConstraint(TableConstraint::Unique { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + AlterTableOperation::AddConstraint(TableConstraint::PrimaryKey { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + AlterTableOperation::AddConstraint(TableConstraint::FulltextOrSpatial { + columns, + .. + }) => columns.first().unwrap().column.expr.clone(), + _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + }, + _ => panic!("Expected CREATE INDEX, ALTER TABLE, or CREATE TABLE, got: {stmt:?}"), + } +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 540348ff..b11d76dd 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -670,6 +670,20 @@ fn table_constraint_unique_primary_ctor( characteristics: Option, unique_index_type_display: Option, ) -> TableConstraint { + let columns = columns + .into_iter() + .map(|ident| IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(ident), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None, + }) + .collect(); match unique_index_type_display { Some(index_type_display) => TableConstraint::Unique { name, @@ -795,6 +809,67 @@ fn parse_create_table_primary_and_unique_key_with_index_options() { } } +#[test] +fn parse_prefix_key_part() { + let expected = vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::value( + number("10"), + )))]; + for sql in [ + "CREATE INDEX idx_index ON t(textcol(10))", + "ALTER TABLE tab ADD INDEX idx_index (textcol(10))", + "ALTER TABLE tab ADD PRIMARY KEY (textcol(10))", + "ALTER TABLE tab ADD UNIQUE KEY (textcol(10))", + "ALTER TABLE tab ADD UNIQUE KEY (textcol(10))", + "ALTER TABLE tab ADD FULLTEXT INDEX (textcol(10))", + "CREATE TABLE t (textcol TEXT, INDEX idx_index (textcol(10)))", + ] { + match index_column(mysql_and_generic().verified_stmt(sql)) { + Expr::Function(Function { + name, + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => { + assert_eq!(name.to_string(), "textcol"); + assert_eq!(args, expected); + } + expr => panic!("unexpected expression {expr} for {sql}"), + } + } +} + +#[test] +fn test_functional_key_part() { + assert_eq!( + index_column( + mysql_and_generic() + .verified_stmt("CREATE INDEX idx_index ON t((col COLLATE utf8mb4_bin) DESC)") + ), + Expr::Nested(Box::new(Expr::Collate { + expr: Box::new(Expr::Identifier("col".into())), + collation: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier( + Ident::new("utf8mb4_bin") + )]), + })) + ); + assert_eq!( + index_column(mysql_and_generic().verified_stmt( + r#"CREATE TABLE t (jsoncol JSON, PRIMARY KEY ((CAST(col ->> '$.id' AS UNSIGNED)) ASC))"# + )), + Expr::Nested(Box::new(Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col"))), + op: BinaryOperator::LongArrow, + right: Box::new(Expr::Value( + Value::SingleQuotedString("$.id".to_string()).with_empty_span() + )), + }), + data_type: DataType::Unsigned, + format: None, + })), + ); +} + #[test] fn parse_create_table_primary_and_unique_key_with_index_type() { let sqls = ["UNIQUE", "PRIMARY KEY"].map(|key_ty| { From 44f3be38e5c9199479a4b986fdfaf417e925f0d4 Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Tue, 24 Jun 2025 09:29:44 +0300 Subject: [PATCH 099/130] fix: parse snowflake fetch clause (#1894) --- src/parser/mod.rs | 11 ++++++----- tests/sqlparser_snowflake.rs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 35c05dfb..a5e89069 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15023,7 +15023,8 @@ impl<'a> Parser<'a> { /// Parse a FETCH clause pub fn parse_fetch(&mut self) -> Result { - self.expect_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT])?; + let _ = self.parse_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]); + let (quantity, percent) = if self .parse_one_of_keywords(&[Keyword::ROW, Keyword::ROWS]) .is_some() @@ -15032,16 +15033,16 @@ impl<'a> Parser<'a> { } else { let quantity = Expr::Value(self.parse_value()?); let percent = self.parse_keyword(Keyword::PERCENT); - self.expect_one_of_keywords(&[Keyword::ROW, Keyword::ROWS])?; + let _ = self.parse_one_of_keywords(&[Keyword::ROW, Keyword::ROWS]); (Some(quantity), percent) }; + let with_ties = if self.parse_keyword(Keyword::ONLY) { false - } else if self.parse_keywords(&[Keyword::WITH, Keyword::TIES]) { - true } else { - return self.expected("one of ONLY or WITH TIES", self.peek_token()); + self.parse_keywords(&[Keyword::WITH, Keyword::TIES]) }; + Ok(Fetch { with_ties, percent, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b11a2cb0..7dc00f9a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4146,3 +4146,22 @@ END assert_eq!(2, exception[1].idents.len()); assert_eq!(2, exception[1].statements.len()); } + +#[test] +fn test_snowflake_fetch_clause_syntax() { + let canonical = "SELECT c1 FROM fetch_test FETCH FIRST 2 ROWS ONLY"; + snowflake().verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH 2", canonical); + + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH FIRST 2", canonical); + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH NEXT 2", canonical); + + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH 2 ROW", canonical); + + snowflake().verified_only_select_with_canonical( + "SELECT c1 FROM fetch_test FETCH FIRST 2 ROWS", + canonical, + ); +} From b9365b3853cf8878284746af07f7781915ae7052 Mon Sep 17 00:00:00 2001 From: ZacJW Date: Tue, 24 Jun 2025 07:39:02 +0100 Subject: [PATCH 100/130] Support procedure argmode (#1901) --- src/ast/ddl.rs | 9 ++++-- src/parser/mod.rs | 15 +++++++++- tests/sqlparser_common.rs | 62 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 6 ++-- 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index e35332da..f81c6fc2 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,7 +30,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, CommentDef, CreateFunctionBody, + display_comma_separated, display_separated, ArgMode, CommentDef, CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, Ident, IndexColumn, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, @@ -1367,11 +1367,16 @@ impl fmt::Display for NullsDistinctOption { pub struct ProcedureParam { pub name: Ident, pub data_type: DataType, + pub mode: Option, } impl fmt::Display for ProcedureParam { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {}", self.name, self.data_type) + if let Some(mode) = &self.mode { + write!(f, "{mode} {} {}", self.name, self.data_type) + } else { + write!(f, "{} {}", self.name, self.data_type) + } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a5e89069..ca658d7d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7624,9 +7624,22 @@ impl<'a> Parser<'a> { } pub fn parse_procedure_param(&mut self) -> Result { + let mode = if self.parse_keyword(Keyword::IN) { + Some(ArgMode::In) + } else if self.parse_keyword(Keyword::OUT) { + Some(ArgMode::Out) + } else if self.parse_keyword(Keyword::INOUT) { + Some(ArgMode::InOut) + } else { + None + }; let name = self.parse_identifier()?; let data_type = self.parse_data_type()?; - Ok(ProcedureParam { name, data_type }) + Ok(ProcedureParam { + name, + data_type, + mode, + }) } pub fn parse_column_def(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 52054604..d45da69a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15356,3 +15356,65 @@ fn check_enforced() { "CREATE TABLE t (a INT, b INT, c INT, CHECK (a > 0) NOT ENFORCED, CHECK (b > 0) ENFORCED, CHECK (c > 0))", ); } + +#[test] +fn parse_create_procedure_with_parameter_modes() { + let sql = r#"CREATE PROCEDURE test_proc (IN a INTEGER, OUT b TEXT, INOUT c TIMESTAMP, d BOOL) AS BEGIN SELECT 1; END"#; + match verified_stmt(sql) { + Statement::CreateProcedure { + or_alter, + name, + params, + .. + } => { + assert_eq!(or_alter, false); + assert_eq!(name.to_string(), "test_proc"); + let fake_span = Span { + start: Location { line: 0, column: 0 }, + end: Location { line: 0, column: 0 }, + }; + assert_eq!( + params, + Some(vec![ + ProcedureParam { + name: Ident { + value: "a".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Integer(None), + mode: Some(ArgMode::In) + }, + ProcedureParam { + name: Ident { + value: "b".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Text, + mode: Some(ArgMode::Out) + }, + ProcedureParam { + name: Ident { + value: "c".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Timestamp(None, TimezoneInfo::None), + mode: Some(ArgMode::InOut) + }, + ProcedureParam { + name: Ident { + value: "d".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Bool, + mode: None + }, + ]) + ); + } + _ => unreachable!(), + } +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2a314502..8edb100a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -153,7 +153,8 @@ fn parse_create_procedure() { quote_style: None, span: Span::empty(), }, - data_type: DataType::Int(None) + data_type: DataType::Int(None), + mode: None, }, ProcedureParam { name: Ident { @@ -164,7 +165,8 @@ fn parse_create_procedure() { data_type: DataType::Varchar(Some(CharacterLength::IntegerLength { length: 256, unit: None - })) + })), + mode: None, } ]), name: ObjectName::from(vec![Ident { From b2ab0061c172aa55e06ece592309a96083d2220d Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Wed, 25 Jun 2025 12:21:59 +0200 Subject: [PATCH 101/130] Fix `impl Ord for Ident` (#1893) --- src/ast/mod.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 17464f4a..ef0c4dc9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -28,6 +28,7 @@ use helpers::{ stmt_data_loading::{FileStagingCommand, StageLoadSelectItemKind}, }; +use core::cmp::Ordering; use core::ops::Deref; use core::{ fmt::{self, Display}, @@ -172,7 +173,7 @@ fn format_statement_list(f: &mut fmt::Formatter, statements: &[Statement]) -> fm } /// An identifier, decomposed into its value or character data and the quote style. -#[derive(Debug, Clone, PartialOrd, Ord)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Ident { @@ -214,6 +215,35 @@ impl core::hash::Hash for Ident { impl Eq for Ident {} +impl PartialOrd for Ident { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Ident { + fn cmp(&self, other: &Self) -> Ordering { + let Ident { + value, + quote_style, + // exhaustiveness check; we ignore spans in ordering + span: _, + } = self; + + let Ident { + value: other_value, + quote_style: other_quote_style, + // exhaustiveness check; we ignore spans in ordering + span: _, + } = other; + + // First compare by value, then by quote_style + value + .cmp(other_value) + .then_with(|| quote_style.cmp(other_quote_style)) + } +} + impl Ident { /// Create a new identifier with the given value and no quotes and an empty span. pub fn new(value: S) -> Self @@ -4214,7 +4244,7 @@ pub enum Statement { /// ```sql /// NOTIFY channel [ , payload ] /// ``` - /// send a notification event together with an optional “payload” string to channel + /// send a notification event together with an optional "payload" string to channel /// /// See Postgres NOTIFY { @@ -9771,6 +9801,8 @@ impl fmt::Display for NullInclusion { #[cfg(test)] mod tests { + use crate::tokenizer::Location; + use super::*; #[test] @@ -10066,4 +10098,16 @@ mod tests { test_steps(OneOrManyWithParens::Many(vec![2]), vec![2], 3); test_steps(OneOrManyWithParens::Many(vec![3, 4]), vec![3, 4], 4); } + + // Tests that the position in the code of an `Ident` does not affect its + // ordering. + #[test] + fn test_ident_ord() { + let mut a = Ident::with_span(Span::new(Location::new(1, 1), Location::new(1, 1)), "a"); + let mut b = Ident::with_span(Span::new(Location::new(2, 2), Location::new(2, 2)), "b"); + + assert!(a < b); + std::mem::swap(&mut a.span, &mut b.span); + assert!(a < b); + } } From 1bbc05cdff7952182e2cdafe55091de0f1f7b3ce Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Wed, 25 Jun 2025 16:10:01 +0200 Subject: [PATCH 102/130] Snowflake: support multiple column options in `CREATE VIEW` (#1891) --- src/ast/ddl.rs | 28 ++++++++++++++++++++++++++-- src/ast/mod.rs | 15 ++++++++------- src/ast/spans.rs | 17 +++++++++++------ src/dialect/mod.rs | 4 ++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 31 ++++++++++++++++++++----------- tests/sqlparser_bigquery.rs | 18 ++++++++++-------- tests/sqlparser_clickhouse.rs | 4 ++-- tests/sqlparser_common.rs | 2 +- tests/sqlparser_snowflake.rs | 15 ++++++++++++--- 10 files changed, 98 insertions(+), 40 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f81c6fc2..d2863c3a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1426,7 +1426,24 @@ impl fmt::Display for ColumnDef { pub struct ViewColumnDef { pub name: Ident, pub data_type: Option, - pub options: Option>, + pub options: Option, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ColumnOptions { + CommaSeparated(Vec), + SpaceSeparated(Vec), +} + +impl ColumnOptions { + pub fn as_slice(&self) -> &[ColumnOption] { + match self { + ColumnOptions::CommaSeparated(options) => options.as_slice(), + ColumnOptions::SpaceSeparated(options) => options.as_slice(), + } + } } impl fmt::Display for ViewColumnDef { @@ -1436,7 +1453,14 @@ impl fmt::Display for ViewColumnDef { write!(f, " {}", data_type)?; } if let Some(options) = self.options.as_ref() { - write!(f, " {}", display_comma_separated(options.as_slice()))?; + match options { + ColumnOptions::CommaSeparated(column_options) => { + write!(f, " {}", display_comma_separated(column_options.as_slice()))?; + } + ColumnOptions::SpaceSeparated(column_options) => { + write!(f, " {}", display_separated(column_options.as_slice(), " "))? + } + } } Ok(()) } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ef0c4dc9..bfed91fb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -61,13 +61,14 @@ pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, Deduplicate, - DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, - ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, TagsColumnOption, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, + ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, + Deduplicate, DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, + Partition, ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, + TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index ca321cc2..78ed772b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use crate::ast::query::SelectItemQualifiedWildcardKind; +use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions}; use core::iter; use crate::tokenizer::Span; @@ -991,10 +991,13 @@ impl Spanned for ViewColumnDef { options, } = self; - union_spans( - core::iter::once(name.span) - .chain(options.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ) + name.span.union_opt(&options.as_ref().map(|o| o.span())) + } +} + +impl Spanned for ColumnOptions { + fn span(&self) -> Span { + union_spans(self.as_slice().iter().map(|i| i.span())) } } @@ -1055,7 +1058,9 @@ impl Spanned for CreateTableOptions { match self { CreateTableOptions::None => Span::empty(), CreateTableOptions::With(vec) => union_spans(vec.iter().map(|i| i.span())), - CreateTableOptions::Options(vec) => union_spans(vec.iter().map(|i| i.span())), + CreateTableOptions::Options(vec) => { + union_spans(vec.as_slice().iter().map(|i| i.span())) + } CreateTableOptions::Plain(vec) => union_spans(vec.iter().map(|i| i.span())), CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())), } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a4c899e6..bc92948d 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1028,6 +1028,10 @@ pub trait Dialect: Debug + Any { fn supports_set_names(&self) -> bool { false } + + fn supports_space_separated_column_options(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 66e04ac2..5ebb7e37 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -356,6 +356,10 @@ impl Dialect for SnowflakeDialect { fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { &RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR } + + fn supports_space_separated_column_options(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ca658d7d..44ba0bb7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10579,17 +10579,7 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier()?; - let options = if (dialect_of!(self is BigQueryDialect | GenericDialect) - && self.parse_keyword(Keyword::OPTIONS)) - || (dialect_of!(self is SnowflakeDialect | GenericDialect) - && self.parse_keyword(Keyword::COMMENT)) - { - self.prev_token(); - self.parse_optional_column_option()? - .map(|option| vec![option]) - } else { - None - }; + let options = self.parse_view_column_options()?; let data_type = if dialect_of!(self is ClickHouseDialect) { Some(self.parse_data_type()?) } else { @@ -10602,6 +10592,25 @@ impl<'a> Parser<'a> { }) } + fn parse_view_column_options(&mut self) -> Result, ParserError> { + let mut options = Vec::new(); + loop { + let option = self.parse_optional_column_option()?; + if let Some(option) = option { + options.push(option); + } else { + break; + } + } + if options.is_empty() { + Ok(None) + } else if self.dialect.supports_space_separated_column_options() { + Ok(Some(ColumnOptions::SpaceSeparated(options))) + } else { + Ok(Some(ColumnOptions::CommaSeparated(options))) + } + } + /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 0de0b12b..2bcdb4e5 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -355,14 +355,16 @@ fn parse_create_view_with_options() { ViewColumnDef { name: Ident::new("age"), data_type: None, - options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue { - key: Ident::new("description"), - value: Expr::Value( - Value::DoubleQuotedString("field age".to_string()).with_span( - Span::new(Location::new(1, 42), Location::new(1, 52)) - ) - ), - }])]), + options: Some(ColumnOptions::CommaSeparated(vec![ColumnOption::Options( + vec![SqlOption::KeyValue { + key: Ident::new("description"), + value: Expr::Value( + Value::DoubleQuotedString("field age".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), + }] + )])), }, ], columns diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 93b4c4f5..ed5d7ad2 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -914,7 +914,7 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, }, ViewColumnDef { name: "f".into(), @@ -926,7 +926,7 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, }, ] ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d45da69a..3c20f82a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7990,7 +7990,7 @@ fn parse_create_view_with_columns() { .map(|name| ViewColumnDef { name, data_type: None, - options: None + options: None, }) .collect::>() ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7dc00f9a..8b3988d9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3124,7 +3124,7 @@ fn view_comment_option_should_be_after_column_list() { "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') COMMENT = 'Comment' AS SELECT a FROM t", "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') WITH (foo = bar) COMMENT = 'Comment' AS SELECT a FROM t", ] { - snowflake_and_generic() + snowflake() .verified_stmt(sql); } } @@ -3133,7 +3133,7 @@ fn view_comment_option_should_be_after_column_list() { fn parse_view_column_descriptions() { let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; - match snowflake_and_generic().verified_stmt(sql) { + match snowflake().verified_stmt(sql) { Statement::CreateView { name, columns, .. } => { assert_eq!(name.to_string(), "v"); assert_eq!( @@ -3142,7 +3142,9 @@ fn parse_view_column_descriptions() { ViewColumnDef { name: Ident::new("a"), data_type: None, - options: Some(vec![ColumnOption::Comment("Comment".to_string())]), + options: Some(ColumnOptions::SpaceSeparated(vec![ColumnOption::Comment( + "Comment".to_string() + )])), }, ViewColumnDef { name: Ident::new("b"), @@ -4165,3 +4167,10 @@ fn test_snowflake_fetch_clause_syntax() { canonical, ); } + +#[test] +fn test_snowflake_create_view_with_multiple_column_options() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} From 95d16e3b2d08ba74ada44dbcfe4ece3400b2d89b Mon Sep 17 00:00:00 2001 From: ZacJW Date: Fri, 27 Jun 2025 17:22:21 +0100 Subject: [PATCH 103/130] Add support for `LANGUAGE` clause in `CREATE PROCEDURE` (#1903) --- src/ast/mod.rs | 6 ++++++ src/parser/mod.rs | 8 ++++++++ tests/sqlparser_common.rs | 30 ++++++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 3 ++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bfed91fb..1ea0b5c2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3945,6 +3945,7 @@ pub enum Statement { or_alter: bool, name: ObjectName, params: Option>, + language: Option, body: ConditionalStatements, }, /// ```sql @@ -4848,6 +4849,7 @@ impl fmt::Display for Statement { name, or_alter, params, + language, body, } => { write!( @@ -4863,6 +4865,10 @@ impl fmt::Display for Statement { } } + if let Some(language) = language { + write!(f, " LANGUAGE {language}")?; + } + write!(f, " AS {body}") } Statement::CreateMacro { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 44ba0bb7..e09b7e33 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15802,6 +15802,13 @@ impl<'a> Parser<'a> { pub fn parse_create_procedure(&mut self, or_alter: bool) -> Result { let name = self.parse_object_name(false)?; let params = self.parse_optional_procedure_parameters()?; + + let language = if self.parse_keyword(Keyword::LANGUAGE) { + Some(self.parse_identifier()?) + } else { + None + }; + self.expect_keyword_is(Keyword::AS)?; let body = self.parse_conditional_statements(&[Keyword::END])?; @@ -15810,6 +15817,7 @@ impl<'a> Parser<'a> { name, or_alter, params, + language, body, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3c20f82a..93cc89e5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15357,6 +15357,36 @@ fn check_enforced() { ); } +#[test] +fn parse_create_procedure_with_language() { + let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#; + match verified_stmt(sql) { + Statement::CreateProcedure { + or_alter, + name, + params, + language, + .. + } => { + assert_eq!(or_alter, false); + assert_eq!(name.to_string(), "test_proc"); + assert_eq!(params, Some(vec![])); + assert_eq!( + language, + Some(Ident { + value: "sql".into(), + quote_style: None, + span: Span { + start: Location::empty(), + end: Location::empty() + } + }) + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_procedure_with_parameter_modes() { let sql = r#"CREATE PROCEDURE test_proc (IN a INTEGER, OUT b TEXT, INOUT c TIMESTAMP, d BOOL) AS BEGIN SELECT 1; END"#; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 8edb100a..9ec28f42 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -173,7 +173,8 @@ fn parse_create_procedure() { value: "test".into(), quote_style: None, span: Span::empty(), - }]) + }]), + language: None, } ) } From 5f2b5fe7beb0698663f9ba9572c711c6bee9ef6e Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Fri, 27 Jun 2025 20:21:17 +0200 Subject: [PATCH 104/130] Fix clippy lints on 1.88.0 (#1910) --- examples/cli.rs | 2 +- sqlparser_bench/benches/sqlparser_bench.rs | 11 ++- src/ast/data_type.rs | 16 ++-- src/ast/dcl.rs | 18 ++-- src/ast/ddl.rs | 28 +++---- src/ast/dml.rs | 12 +-- src/ast/helpers/key_value_options.rs | 2 +- src/ast/mod.rs | 96 +++++++++++----------- src/ast/query.rs | 76 ++++++++--------- src/ast/value.rs | 6 +- src/ast/visitor.rs | 4 +- src/dialect/mod.rs | 2 +- src/dialect/postgresql.rs | 2 +- src/parser/mod.rs | 13 ++- src/test_utils.rs | 2 +- src/tokenizer.rs | 10 +-- tests/sqlparser_clickhouse.rs | 6 +- tests/sqlparser_common.rs | 20 ++--- tests/sqlparser_databricks.rs | 12 +-- tests/sqlparser_duckdb.rs | 10 ++- tests/sqlparser_hive.rs | 4 +- tests/sqlparser_mssql.rs | 10 +-- tests/sqlparser_mysql.rs | 47 ++++++----- tests/sqlparser_postgres.rs | 11 ++- tests/sqlparser_snowflake.rs | 32 ++++---- tests/sqlparser_sqlite.rs | 6 +- 26 files changed, 226 insertions(+), 232 deletions(-) diff --git a/examples/cli.rs b/examples/cli.rs index 0252fca7..08a40a6d 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -63,7 +63,7 @@ $ cargo run --example cli - [--dialectname] }; let contents = if filename == "-" { - println!("Parsing from stdin using {:?}", dialect); + println!("Parsing from stdin using {dialect:?}"); let mut buf = Vec::new(); stdin() .read_to_end(&mut buf) diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs index 24c59c07..6132ee43 100644 --- a/sqlparser_bench/benches/sqlparser_bench.rs +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -45,25 +45,24 @@ fn basic_queries(c: &mut Criterion) { let large_statement = { let expressions = (0..1000) - .map(|n| format!("FN_{}(COL_{})", n, n)) + .map(|n| format!("FN_{n}(COL_{n})")) .collect::>() .join(", "); let tables = (0..1000) - .map(|n| format!("TABLE_{}", n)) + .map(|n| format!("TABLE_{n}")) .collect::>() .join(" JOIN "); let where_condition = (0..1000) - .map(|n| format!("COL_{} = {}", n, n)) + .map(|n| format!("COL_{n} = {n}")) .collect::>() .join(" OR "); let order_condition = (0..1000) - .map(|n| format!("COL_{} DESC", n)) + .map(|n| format!("COL_{n} DESC")) .collect::>() .join(", "); format!( - "SELECT {} FROM {} WHERE {} ORDER BY {}", - expressions, tables, where_condition, order_condition + "SELECT {expressions} FROM {tables} WHERE {where_condition} ORDER BY {order_condition}" ) }; diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index ef04b7e4..0897f2db 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -666,7 +666,7 @@ impl fmt::Display for DataType { } DataType::Enum(vals, bits) => { match bits { - Some(bits) => write!(f, "ENUM{}", bits), + Some(bits) => write!(f, "ENUM{bits}"), None => write!(f, "ENUM"), }?; write!(f, "(")?; @@ -714,16 +714,16 @@ impl fmt::Display for DataType { } // ClickHouse DataType::Nullable(data_type) => { - write!(f, "Nullable({})", data_type) + write!(f, "Nullable({data_type})") } DataType::FixedString(character_length) => { - write!(f, "FixedString({})", character_length) + write!(f, "FixedString({character_length})") } DataType::LowCardinality(data_type) => { - write!(f, "LowCardinality({})", data_type) + write!(f, "LowCardinality({data_type})") } DataType::Map(key_data_type, value_data_type) => { - write!(f, "Map({}, {})", key_data_type, value_data_type) + write!(f, "Map({key_data_type}, {value_data_type})") } DataType::Tuple(fields) => { write!(f, "Tuple({})", display_comma_separated(fields)) @@ -745,7 +745,7 @@ impl fmt::Display for DataType { DataType::NamedTable { name, columns } => { write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) } - DataType::GeometricType(kind) => write!(f, "{}", kind), + DataType::GeometricType(kind) => write!(f, "{kind}"), DataType::TsVector => write!(f, "TSVECTOR"), DataType::TsQuery => write!(f, "TSQUERY"), } @@ -942,7 +942,7 @@ impl fmt::Display for CharacterLength { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CharacterLength::IntegerLength { length, unit } => { - write!(f, "{}", length)?; + write!(f, "{length}")?; if let Some(unit) = unit { write!(f, " {unit}")?; } @@ -997,7 +997,7 @@ impl fmt::Display for BinaryLength { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { BinaryLength::IntegerLength { length } => { - write!(f, "{}", length)?; + write!(f, "{length}")?; } BinaryLength::Max => { write!(f, "MAX")?; diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 735ab0cc..07989407 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -173,7 +173,7 @@ impl fmt::Display for AlterRoleOperation { in_database, } => { if let Some(database_name) = in_database { - write!(f, "IN DATABASE {} ", database_name)?; + write!(f, "IN DATABASE {database_name} ")?; } match config_value { @@ -187,7 +187,7 @@ impl fmt::Display for AlterRoleOperation { in_database, } => { if let Some(database_name) = in_database { - write!(f, "IN DATABASE {} ", database_name)?; + write!(f, "IN DATABASE {database_name} ")?; } match config_name { @@ -218,15 +218,15 @@ impl fmt::Display for Use { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("USE ")?; match self { - Use::Catalog(name) => write!(f, "CATALOG {}", name), - Use::Schema(name) => write!(f, "SCHEMA {}", name), - Use::Database(name) => write!(f, "DATABASE {}", name), - Use::Warehouse(name) => write!(f, "WAREHOUSE {}", name), - Use::Role(name) => write!(f, "ROLE {}", name), + Use::Catalog(name) => write!(f, "CATALOG {name}"), + Use::Schema(name) => write!(f, "SCHEMA {name}"), + Use::Database(name) => write!(f, "DATABASE {name}"), + Use::Warehouse(name) => write!(f, "WAREHOUSE {name}"), + Use::Role(name) => write!(f, "ROLE {name}"), Use::SecondaryRoles(secondary_roles) => { - write!(f, "SECONDARY ROLES {}", secondary_roles) + write!(f, "SECONDARY ROLES {secondary_roles}") } - Use::Object(name) => write!(f, "{}", name), + Use::Object(name) => write!(f, "{name}"), Use::Default => write!(f, "DEFAULT"), } } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d2863c3a..afe9f228 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -57,7 +57,7 @@ impl fmt::Display for ReplicaIdentity { ReplicaIdentity::None => f.write_str("NONE"), ReplicaIdentity::Full => f.write_str("FULL"), ReplicaIdentity::Default => f.write_str("DEFAULT"), - ReplicaIdentity::Index(idx) => write!(f, "USING INDEX {}", idx), + ReplicaIdentity::Index(idx) => write!(f, "USING INDEX {idx}"), } } } @@ -450,7 +450,7 @@ pub enum Owner { impl fmt::Display for Owner { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Owner::Ident(ident) => write!(f, "{}", ident), + Owner::Ident(ident) => write!(f, "{ident}"), Owner::CurrentRole => write!(f, "CURRENT_ROLE"), Owner::CurrentUser => write!(f, "CURRENT_USER"), Owner::SessionUser => write!(f, "SESSION_USER"), @@ -525,7 +525,7 @@ impl fmt::Display for AlterTableOperation { if *if_not_exists { write!(f, " IF NOT EXISTS")?; } - write!(f, " {} ({})", name, query) + write!(f, " {name} ({query})") } AlterTableOperation::Algorithm { equals, algorithm } => { write!( @@ -540,7 +540,7 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name) + write!(f, " {name}") } AlterTableOperation::MaterializeProjection { if_exists, @@ -551,9 +551,9 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name)?; + write!(f, " {name}")?; if let Some(partition) = partition { - write!(f, " IN PARTITION {}", partition)?; + write!(f, " IN PARTITION {partition}")?; } Ok(()) } @@ -566,9 +566,9 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name)?; + write!(f, " {name}")?; if let Some(partition) = partition { - write!(f, " IN PARTITION {}", partition)?; + write!(f, " IN PARTITION {partition}")?; } Ok(()) } @@ -1168,7 +1168,7 @@ impl fmt::Display for TableConstraint { write!(f, " ON UPDATE {action}")?; } if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } @@ -1308,7 +1308,7 @@ impl fmt::Display for IndexType { Self::SPGiST => write!(f, "SPGIST"), Self::BRIN => write!(f, "BRIN"), Self::Bloom => write!(f, "BLOOM"), - Self::Custom(name) => write!(f, "{}", name), + Self::Custom(name) => write!(f, "{name}"), } } } @@ -1450,7 +1450,7 @@ impl fmt::Display for ViewColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; if let Some(data_type) = self.data_type.as_ref() { - write!(f, " {}", data_type)?; + write!(f, " {data_type}")?; } if let Some(options) = self.options.as_ref() { match options { @@ -1845,7 +1845,7 @@ impl fmt::Display for ColumnOption { } => { write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" })?; if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } @@ -1867,7 +1867,7 @@ impl fmt::Display for ColumnOption { write!(f, " ON UPDATE {action}")?; } if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } @@ -1927,7 +1927,7 @@ impl fmt::Display for ColumnOption { write!(f, "{parameters}") } OnConflict(keyword) => { - write!(f, "ON CONFLICT {:?}", keyword)?; + write!(f, "ON CONFLICT {keyword:?}")?; Ok(()) } Policy(parameters) => { diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 292650c8..e179f5d7 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -55,7 +55,7 @@ impl Display for IndexColumn { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.column)?; if let Some(operator_class) = &self.operator_class { - write!(f, " {}", operator_class)?; + write!(f, " {operator_class}")?; } Ok(()) } @@ -266,7 +266,7 @@ impl Display for CreateTable { name = self.name, )?; if let Some(on_cluster) = &self.on_cluster { - write!(f, " ON CLUSTER {}", on_cluster)?; + write!(f, " ON CLUSTER {on_cluster}")?; } if !self.columns.is_empty() || !self.constraints.is_empty() { f.write_str(" (")?; @@ -383,15 +383,15 @@ impl Display for CreateTable { match &self.table_options { options @ CreateTableOptions::With(_) | options @ CreateTableOptions::Plain(_) - | options @ CreateTableOptions::TableProperties(_) => write!(f, " {}", options)?, + | options @ CreateTableOptions::TableProperties(_) => write!(f, " {options}")?, _ => (), } if let Some(primary_key) = &self.primary_key { - write!(f, " PRIMARY KEY {}", primary_key)?; + write!(f, " PRIMARY KEY {primary_key}")?; } if let Some(order_by) = &self.order_by { - write!(f, " ORDER BY {}", order_by)?; + write!(f, " ORDER BY {order_by}")?; } if let Some(inherits) = &self.inherits { write!(f, " INHERITS ({})", display_comma_separated(inherits))?; @@ -403,7 +403,7 @@ impl Display for CreateTable { write!(f, " CLUSTER BY {cluster_by}")?; } if let options @ CreateTableOptions::Options(_) = &self.table_options { - write!(f, " {}", options)?; + write!(f, " {options}")?; } if let Some(external_volume) = self.external_volume.as_ref() { write!(f, " EXTERNAL_VOLUME = '{external_volume}'")?; diff --git a/src/ast/helpers/key_value_options.rs b/src/ast/helpers/key_value_options.rs index 06f028dd..796bfd5e 100644 --- a/src/ast/helpers/key_value_options.rs +++ b/src/ast/helpers/key_value_options.rs @@ -67,7 +67,7 @@ impl fmt::Display for KeyValueOptions { } else { f.write_str(" ")?; } - write!(f, "{}", option)?; + write!(f, "{option}")?; } } Ok(()) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1ea0b5c2..0f682062 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -357,7 +357,7 @@ impl ObjectNamePart { impl fmt::Display for ObjectNamePart { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - ObjectNamePart::Identifier(ident) => write!(f, "{}", ident), + ObjectNamePart::Identifier(ident) => write!(f, "{ident}"), } } } @@ -1210,8 +1210,8 @@ pub enum AccessExpr { impl fmt::Display for AccessExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - AccessExpr::Dot(expr) => write!(f, ".{}", expr), - AccessExpr::Subscript(subscript) => write!(f, "[{}]", subscript), + AccessExpr::Dot(expr) => write!(f, ".{expr}"), + AccessExpr::Subscript(subscript) => write!(f, "[{subscript}]"), } } } @@ -1413,12 +1413,12 @@ impl fmt::Display for Expr { match self { Expr::Identifier(s) => write!(f, "{s}"), Expr::Wildcard(_) => f.write_str("*"), - Expr::QualifiedWildcard(prefix, _) => write!(f, "{}.*", prefix), + Expr::QualifiedWildcard(prefix, _) => write!(f, "{prefix}.*"), Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), Expr::CompoundFieldAccess { root, access_chain } => { - write!(f, "{}", root)?; + write!(f, "{root}")?; for field in access_chain { - write!(f, "{}", field)?; + write!(f, "{field}")?; } Ok(()) } @@ -1547,7 +1547,7 @@ impl fmt::Display for Expr { } => { let not_ = if *negated { "NOT " } else { "" }; if form.is_none() { - write!(f, "{} IS {}NORMALIZED", expr, not_) + write!(f, "{expr} IS {not_}NORMALIZED") } else { write!( f, @@ -1869,7 +1869,7 @@ impl fmt::Display for Expr { } } Expr::Named { expr, name } => { - write!(f, "{} AS {}", expr, name) + write!(f, "{expr} AS {name}") } Expr::Dictionary(fields) => { write!(f, "{{{}}}", display_comma_separated(fields)) @@ -2425,7 +2425,7 @@ impl fmt::Display for ConditionalStatements { } Ok(()) } - ConditionalStatements::BeginEnd(bes) => write!(f, "{}", bes), + ConditionalStatements::BeginEnd(bes) => write!(f, "{bes}"), } } } @@ -2945,9 +2945,7 @@ impl Display for Set { write!( f, "SET {modifier}ROLE {role_name}", - modifier = context_modifier - .map(|m| format!("{}", m)) - .unwrap_or_default() + modifier = context_modifier.map(|m| format!("{m}")).unwrap_or_default() ) } Self::SetSessionParam(kind) => write!(f, "SET {kind}"), @@ -2980,7 +2978,7 @@ impl Display for Set { charset_name, collation_name, } => { - write!(f, "SET NAMES {}", charset_name)?; + write!(f, "SET NAMES {charset_name}")?; if let Some(collation) = collation_name { f.write_str(" COLLATE ")?; @@ -3003,7 +3001,7 @@ impl Display for Set { write!( f, "SET {}{}{} = {}", - scope.map(|s| format!("{}", s)).unwrap_or_default(), + scope.map(|s| format!("{s}")).unwrap_or_default(), if *hivevar { "HIVEVAR:" } else { "" }, variable, display_comma_separated(values) @@ -4405,7 +4403,7 @@ impl fmt::Display for Statement { write!(f, "{describe_alias} ")?; if let Some(format) = hive_format { - write!(f, "{} ", format)?; + write!(f, "{format} ")?; } if *has_table_keyword { write!(f, "TABLE ")?; @@ -5241,7 +5239,7 @@ impl fmt::Display for Statement { if *only { write!(f, "ONLY ")?; } - write!(f, "{name} ", name = name)?; + write!(f, "{name} ")?; if let Some(cluster) = on_cluster { write!(f, "ON CLUSTER {cluster} ")?; } @@ -5319,7 +5317,7 @@ impl fmt::Display for Statement { )?; if !session_params.options.is_empty() { if *set { - write!(f, " {}", session_params)?; + write!(f, " {session_params}")?; } else { let options = session_params .options @@ -5353,7 +5351,7 @@ impl fmt::Display for Statement { if *purge { " PURGE" } else { "" }, )?; if let Some(table_name) = table.as_ref() { - write!(f, " ON {}", table_name)?; + write!(f, " ON {table_name}")?; }; Ok(()) } @@ -5608,7 +5606,7 @@ impl fmt::Display for Statement { } => { if *syntax_begin { if let Some(modifier) = *modifier { - write!(f, "BEGIN {}", modifier)?; + write!(f, "BEGIN {modifier}")?; } else { write!(f, "BEGIN")?; } @@ -5644,7 +5642,7 @@ impl fmt::Display for Statement { if *end_syntax { write!(f, "END")?; if let Some(modifier) = *modifier { - write!(f, " {}", modifier)?; + write!(f, " {modifier}")?; } if *chain { write!(f, " AND CHAIN")?; @@ -5743,7 +5741,7 @@ impl fmt::Display for Statement { write!(f, " GRANTED BY {grantor}")?; } if let Some(cascade) = cascade { - write!(f, " {}", cascade)?; + write!(f, " {cascade}")?; } Ok(()) } @@ -5922,13 +5920,13 @@ impl fmt::Display for Statement { if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, )?; if !directory_table_params.options.is_empty() { - write!(f, " DIRECTORY=({})", directory_table_params)?; + write!(f, " DIRECTORY=({directory_table_params})")?; } if !file_format.options.is_empty() { - write!(f, " FILE_FORMAT=({})", file_format)?; + write!(f, " FILE_FORMAT=({file_format})")?; } if !copy_options.options.is_empty() { - write!(f, " COPY_OPTIONS=({})", copy_options)?; + write!(f, " COPY_OPTIONS=({copy_options})")?; } if comment.is_some() { write!(f, " COMMENT='{}'", comment.as_ref().unwrap())?; @@ -5951,7 +5949,7 @@ impl fmt::Display for Statement { validation_mode, partition, } => { - write!(f, "COPY INTO {}", into)?; + write!(f, "COPY INTO {into}")?; if let Some(into_columns) = into_columns { write!(f, " ({})", display_comma_separated(into_columns))?; } @@ -5967,12 +5965,12 @@ impl fmt::Display for Statement { )?; } if let Some(from_obj_alias) = from_obj_alias { - write!(f, " AS {}", from_obj_alias)?; + write!(f, " AS {from_obj_alias}")?; } write!(f, ")")?; } else if let Some(from_obj) = from_obj { // Standard data load - write!(f, " FROM {}{}", from_obj, stage_params)?; + write!(f, " FROM {from_obj}{stage_params}")?; if let Some(from_obj_alias) = from_obj_alias { write!(f, " AS {from_obj_alias}")?; } @@ -5985,24 +5983,24 @@ impl fmt::Display for Statement { write!(f, " FILES = ('{}')", display_separated(files, "', '"))?; } if let Some(pattern) = pattern { - write!(f, " PATTERN = '{}'", pattern)?; + write!(f, " PATTERN = '{pattern}'")?; } if let Some(partition) = partition { write!(f, " PARTITION BY {partition}")?; } if !file_format.options.is_empty() { - write!(f, " FILE_FORMAT=({})", file_format)?; + write!(f, " FILE_FORMAT=({file_format})")?; } if !copy_options.options.is_empty() { match kind { CopyIntoSnowflakeKind::Table => { - write!(f, " COPY_OPTIONS=({})", copy_options)? + write!(f, " COPY_OPTIONS=({copy_options})")? } CopyIntoSnowflakeKind::Location => write!(f, " {copy_options}")?, } } if let Some(validation_mode) = validation_mode { - write!(f, " VALIDATION_MODE = {}", validation_mode)?; + write!(f, " VALIDATION_MODE = {validation_mode}")?; } Ok(()) } @@ -6048,10 +6046,10 @@ impl fmt::Display for Statement { } => { write!(f, "OPTIMIZE TABLE {name}")?; if let Some(on_cluster) = on_cluster { - write!(f, " ON CLUSTER {on_cluster}", on_cluster = on_cluster)?; + write!(f, " ON CLUSTER {on_cluster}")?; } if let Some(partition) = partition { - write!(f, " {partition}", partition = partition)?; + write!(f, " {partition}")?; } if *include_final { write!(f, " FINAL")?; @@ -6178,7 +6176,7 @@ impl fmt::Display for SetAssignment { write!( f, "{}{} = {}", - self.scope.map(|s| format!("{}", s)).unwrap_or_default(), + self.scope.map(|s| format!("{s}")).unwrap_or_default(), self.name, self.value ) @@ -6907,7 +6905,7 @@ impl fmt::Display for GranteeName { match self { GranteeName::ObjectName(name) => name.fmt(f), GranteeName::UserHost { user, host } => { - write!(f, "{}@{}", user, host) + write!(f, "{user}@{host}") } } } @@ -7077,7 +7075,7 @@ pub enum AssignmentTarget { impl fmt::Display for AssignmentTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - AssignmentTarget::ColumnName(column) => write!(f, "{}", column), + AssignmentTarget::ColumnName(column) => write!(f, "{column}"), AssignmentTarget::Tuple(columns) => write!(f, "({})", display_comma_separated(columns)), } } @@ -7322,8 +7320,8 @@ impl fmt::Display for FunctionArguments { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FunctionArguments::None => Ok(()), - FunctionArguments::Subquery(query) => write!(f, "({})", query), - FunctionArguments::List(args) => write!(f, "({})", args), + FunctionArguments::Subquery(query) => write!(f, "({query})"), + FunctionArguments::List(args) => write!(f, "({args})"), } } } @@ -7344,7 +7342,7 @@ pub struct FunctionArgumentList { impl fmt::Display for FunctionArgumentList { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(duplicate_treatment) = self.duplicate_treatment { - write!(f, "{} ", duplicate_treatment)?; + write!(f, "{duplicate_treatment} ")?; } write!(f, "{}", display_comma_separated(&self.args))?; if !self.clauses.is_empty() { @@ -7404,7 +7402,7 @@ impl fmt::Display for FunctionArgumentClause { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FunctionArgumentClause::IgnoreOrRespectNulls(null_treatment) => { - write!(f, "{}", null_treatment) + write!(f, "{null_treatment}") } FunctionArgumentClause::OrderBy(order_by) => { write!(f, "ORDER BY {}", display_comma_separated(order_by)) @@ -7860,12 +7858,12 @@ pub enum SqlOption { impl fmt::Display for SqlOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - SqlOption::Clustered(c) => write!(f, "{}", c), + SqlOption::Clustered(c) => write!(f, "{c}"), SqlOption::Ident(ident) => { - write!(f, "{}", ident) + write!(f, "{ident}") } SqlOption::KeyValue { key: name, value } => { - write!(f, "{} = {}", name, value) + write!(f, "{name} = {value}") } SqlOption::Partition { column_name, @@ -7905,7 +7903,7 @@ impl fmt::Display for SqlOption { SqlOption::NamedParenthesizedList(value) => { write!(f, "{} = ", value.key)?; if let Some(key) = &value.name { - write!(f, "{}", key)?; + write!(f, "{key}")?; } if !value.values.is_empty() { write!(f, "({})", display_comma_separated(&value.values))? @@ -7962,7 +7960,7 @@ impl fmt::Display for AttachDuckDBDatabaseOption { AttachDuckDBDatabaseOption::ReadOnly(Some(true)) => write!(f, "READ_ONLY true"), AttachDuckDBDatabaseOption::ReadOnly(Some(false)) => write!(f, "READ_ONLY false"), AttachDuckDBDatabaseOption::ReadOnly(None) => write!(f, "READ_ONLY"), - AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {}", t), + AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {t}"), } } } @@ -9485,10 +9483,10 @@ impl fmt::Display for ShowStatementIn { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.clause)?; if let Some(parent_type) = &self.parent_type { - write!(f, " {}", parent_type)?; + write!(f, " {parent_type}")?; } if let Some(parent_name) = &self.parent_name { - write!(f, " {}", parent_name)?; + write!(f, " {parent_name}")?; } Ok(()) } @@ -9569,7 +9567,7 @@ impl fmt::Display for TableObject { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::TableName(table_name) => write!(f, "{table_name}"), - Self::TableFunction(func) => write!(f, "FUNCTION {}", func), + Self::TableFunction(func) => write!(f, "FUNCTION {func}"), } } } @@ -9757,7 +9755,7 @@ pub struct ReturnStatement { impl fmt::Display for ReturnStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.value { - Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {}", expr), + Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {expr}"), None => write!(f, "RETURN"), } } diff --git a/src/ast/query.rs b/src/ast/query.rs index 1fb93b6c..c79ec110 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1183,7 +1183,7 @@ impl fmt::Display for TableIndexHints { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {} ", self.hint_type, self.index_type)?; if let Some(for_clause) = &self.for_clause { - write!(f, "FOR {} ", for_clause)?; + write!(f, "FOR {for_clause} ")?; } write!(f, "({})", display_comma_separated(&self.index_names)) } @@ -1459,7 +1459,7 @@ impl fmt::Display for TableSampleQuantity { } write!(f, "{}", self.value)?; if let Some(unit) = &self.unit { - write!(f, " {}", unit)?; + write!(f, " {unit}")?; } if self.parenthesized { write!(f, ")")?; @@ -1552,7 +1552,7 @@ impl fmt::Display for TableSampleBucket { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "BUCKET {} OUT OF {}", self.bucket, self.total)?; if let Some(on) = &self.on { - write!(f, " ON {}", on)?; + write!(f, " ON {on}")?; } Ok(()) } @@ -1561,19 +1561,19 @@ impl fmt::Display for TableSample { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.modifier)?; if let Some(name) = &self.name { - write!(f, " {}", name)?; + write!(f, " {name}")?; } if let Some(quantity) = &self.quantity { - write!(f, " {}", quantity)?; + write!(f, " {quantity}")?; } if let Some(seed) = &self.seed { - write!(f, " {}", seed)?; + write!(f, " {seed}")?; } if let Some(bucket) = &self.bucket { - write!(f, " ({})", bucket)?; + write!(f, " ({bucket})")?; } if let Some(offset) = &self.offset { - write!(f, " OFFSET {}", offset)?; + write!(f, " OFFSET {offset}")?; } Ok(()) } @@ -1651,7 +1651,7 @@ impl fmt::Display for RowsPerMatch { RowsPerMatch::AllRows(mode) => { write!(f, "ALL ROWS PER MATCH")?; if let Some(mode) = mode { - write!(f, " {}", mode)?; + write!(f, " {mode}")?; } Ok(()) } @@ -1777,7 +1777,7 @@ impl fmt::Display for MatchRecognizePattern { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use MatchRecognizePattern::*; match self { - Symbol(symbol) => write!(f, "{}", symbol), + Symbol(symbol) => write!(f, "{symbol}"), Exclude(symbol) => write!(f, "{{- {symbol} -}}"), Permute(symbols) => write!(f, "PERMUTE({})", display_comma_separated(symbols)), Concat(patterns) => write!(f, "{}", display_separated(patterns, " ")), @@ -2148,7 +2148,7 @@ impl fmt::Display for TableAliasColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; if let Some(ref data_type) = self.data_type { - write!(f, " {}", data_type)?; + write!(f, " {data_type}")?; } Ok(()) } @@ -2398,7 +2398,7 @@ impl fmt::Display for OrderBy { write!(f, " {}", display_comma_separated(exprs))?; } OrderByKind::All(all) => { - write!(f, " ALL{}", all)?; + write!(f, " ALL{all}")?; } } @@ -2429,7 +2429,7 @@ impl fmt::Display for OrderByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", self.expr, self.options)?; if let Some(ref with_fill) = self.with_fill { - write!(f, " {}", with_fill)? + write!(f, " {with_fill}")? } Ok(()) } @@ -2452,13 +2452,13 @@ impl fmt::Display for WithFill { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "WITH FILL")?; if let Some(ref from) = self.from { - write!(f, " FROM {}", from)?; + write!(f, " FROM {from}")?; } if let Some(ref to) = self.to { - write!(f, " TO {}", to)?; + write!(f, " TO {to}")?; } if let Some(ref step) = self.step { - write!(f, " STEP {}", step)?; + write!(f, " STEP {step}")?; } Ok(()) } @@ -2487,7 +2487,7 @@ impl fmt::Display for InterpolateExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.column)?; if let Some(ref expr) = self.expr { - write!(f, " AS {}", expr)?; + write!(f, " AS {expr}")?; } Ok(()) } @@ -2565,7 +2565,7 @@ impl fmt::Display for LimitClause { Ok(()) } LimitClause::OffsetCommaLimit { offset, limit } => { - write!(f, " LIMIT {}, {}", offset, limit) + write!(f, " LIMIT {offset}, {limit}") } } } @@ -2702,12 +2702,12 @@ impl fmt::Display for PipeOperator { write!(f, "DROP {}", display_comma_separated(columns.as_slice())) } PipeOperator::As { alias } => { - write!(f, "AS {}", alias) + write!(f, "AS {alias}") } PipeOperator::Limit { expr, offset } => { - write!(f, "LIMIT {}", expr)?; + write!(f, "LIMIT {expr}")?; if let Some(offset) = offset { - write!(f, " OFFSET {}", offset)?; + write!(f, " OFFSET {offset}")?; } Ok(()) } @@ -2730,14 +2730,14 @@ impl fmt::Display for PipeOperator { } PipeOperator::Where { expr } => { - write!(f, "WHERE {}", expr) + write!(f, "WHERE {expr}") } PipeOperator::OrderBy { exprs } => { write!(f, "ORDER BY {}", display_comma_separated(exprs.as_slice())) } PipeOperator::TableSample { sample } => { - write!(f, "{}", sample) + write!(f, "{sample}") } } } @@ -3016,7 +3016,7 @@ pub enum FormatClause { impl fmt::Display for FormatClause { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - FormatClause::Identifier(ident) => write!(f, "FORMAT {}", ident), + FormatClause::Identifier(ident) => write!(f, "FORMAT {ident}"), FormatClause::Null => write!(f, "FORMAT NULL"), } } @@ -3078,9 +3078,9 @@ impl fmt::Display for ForClause { without_array_wrapper, } => { write!(f, "FOR JSON ")?; - write!(f, "{}", for_json)?; + write!(f, "{for_json}")?; if let Some(root) = root { - write!(f, ", ROOT('{}')", root)?; + write!(f, ", ROOT('{root}')")?; } if *include_null_values { write!(f, ", INCLUDE_NULL_VALUES")?; @@ -3098,7 +3098,7 @@ impl fmt::Display for ForClause { r#type, } => { write!(f, "FOR XML ")?; - write!(f, "{}", for_xml)?; + write!(f, "{for_xml}")?; if *binary_base64 { write!(f, ", BINARY BASE64")?; } @@ -3106,7 +3106,7 @@ impl fmt::Display for ForClause { write!(f, ", TYPE")?; } if let Some(root) = root { - write!(f, ", ROOT('{}')", root)?; + write!(f, ", ROOT('{root}')")?; } if *elements { write!(f, ", ELEMENTS")?; @@ -3133,7 +3133,7 @@ impl fmt::Display for ForXml { ForXml::Raw(root) => { write!(f, "RAW")?; if let Some(root) = root { - write!(f, "('{}')", root)?; + write!(f, "('{root}')")?; } Ok(()) } @@ -3142,7 +3142,7 @@ impl fmt::Display for ForXml { ForXml::Path(root) => { write!(f, "PATH")?; if let Some(root) = root { - write!(f, "('{}')", root)?; + write!(f, "('{root}')")?; } Ok(()) } @@ -3205,7 +3205,7 @@ impl fmt::Display for JsonTableColumn { JsonTableColumn::Named(json_table_named_column) => { write!(f, "{json_table_named_column}") } - JsonTableColumn::ForOrdinality(ident) => write!(f, "{} FOR ORDINALITY", ident), + JsonTableColumn::ForOrdinality(ident) => write!(f, "{ident} FOR ORDINALITY"), JsonTableColumn::Nested(json_table_nested_column) => { write!(f, "{json_table_nested_column}") } @@ -3271,10 +3271,10 @@ impl fmt::Display for JsonTableNamedColumn { self.path )?; if let Some(on_empty) = &self.on_empty { - write!(f, " {} ON EMPTY", on_empty)?; + write!(f, " {on_empty} ON EMPTY")?; } if let Some(on_error) = &self.on_error { - write!(f, " {} ON ERROR", on_error)?; + write!(f, " {on_error} ON ERROR")?; } Ok(()) } @@ -3296,7 +3296,7 @@ impl fmt::Display for JsonTableColumnErrorHandling { match self { JsonTableColumnErrorHandling::Null => write!(f, "NULL"), JsonTableColumnErrorHandling::Default(json_string) => { - write!(f, "DEFAULT {}", json_string) + write!(f, "DEFAULT {json_string}") } JsonTableColumnErrorHandling::Error => write!(f, "ERROR"), } @@ -3429,12 +3429,12 @@ impl fmt::Display for XmlTableColumn { default, nullable, } => { - write!(f, " {}", r#type)?; + write!(f, " {type}")?; if let Some(p) = path { - write!(f, " PATH {}", p)?; + write!(f, " PATH {p}")?; } if let Some(d) = default { - write!(f, " DEFAULT {}", d)?; + write!(f, " DEFAULT {d}")?; } if !*nullable { write!(f, " NOT NULL")?; @@ -3465,7 +3465,7 @@ impl fmt::Display for XmlPassingArgument { } write!(f, "{}", self.expr)?; if let Some(alias) = &self.alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } Ok(()) } diff --git a/src/ast/value.rs b/src/ast/value.rs index 98616407..90dbccbf 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -551,16 +551,16 @@ impl fmt::Display for EscapeUnicodeStringLiteral<'_> { write!(f, r#"\\"#)?; } x if x.is_ascii() => { - write!(f, "{}", c)?; + write!(f, "{c}")?; } _ => { let codepoint = c as u32; // if the character fits in 32 bits, we can use the \XXXX format // otherwise, we need to use the \+XXXXXX format if codepoint <= 0xFFFF { - write!(f, "\\{:04X}", codepoint)?; + write!(f, "\\{codepoint:04X}")?; } else { - write!(f, "\\+{:06X}", codepoint)?; + write!(f, "\\+{codepoint:06X}")?; } } } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index ab4f73aa..8e0a3139 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -926,10 +926,10 @@ mod tests { #[test] fn overflow() { let cond = (0..1000) - .map(|n| format!("X = {}", n)) + .map(|n| format!("X = {n}")) .collect::>() .join(" OR "); - let sql = format!("SELECT x where {0}", cond); + let sql = format!("SELECT x where {cond}"); let dialect = GenericDialect {}; let tokens = Tokenizer::new(&dialect, sql.as_str()).tokenize().unwrap(); diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index bc92948d..c79b4517 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -587,7 +587,7 @@ pub trait Dialect: Debug + Any { } let token = parser.peek_token(); - debug!("get_next_precedence_full() {:?}", token); + debug!("get_next_precedence_full() {token:?}"); match token.token { Token::Word(w) if w.keyword == Keyword::OR => Ok(p!(Or)), Token::Word(w) if w.keyword == Keyword::AND => Ok(p!(And)), diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 9b08b8f3..a91ab598 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -104,7 +104,7 @@ impl Dialect for PostgreSqlDialect { fn get_next_precedence(&self, parser: &Parser) -> Option> { let token = parser.peek_token(); - debug!("get_next_precedence() {:?}", token); + debug!("get_next_precedence() {token:?}"); // we only return some custom value here when the behaviour (not merely the numeric value) differs // from the default implementation diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e09b7e33..f1b09afb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -436,7 +436,7 @@ impl<'a> Parser<'a> { /// /// See example on [`Parser::new()`] for an example pub fn try_with_sql(self, sql: &str) -> Result { - debug!("Parsing sql '{}'...", sql); + debug!("Parsing sql '{sql}'..."); let tokens = Tokenizer::new(self.dialect, sql) .with_unescape(self.options.unescape) .tokenize_with_location()?; @@ -1226,10 +1226,10 @@ impl<'a> Parser<'a> { expr = self.parse_compound_expr(expr, vec![])?; - debug!("prefix: {:?}", expr); + debug!("prefix: {expr:?}"); loop { let next_precedence = self.get_next_precedence()?; - debug!("next precedence: {:?}", next_precedence); + debug!("next precedence: {next_precedence:?}"); if precedence >= next_precedence { break; @@ -1631,8 +1631,7 @@ impl<'a> Parser<'a> { Token::QuestionPipe => UnaryOperator::QuestionPipe, _ => { return Err(ParserError::ParserError(format!( - "Unexpected token in unary operator parsing: {:?}", - tok + "Unexpected token in unary operator parsing: {tok:?}" ))) } }; @@ -13655,7 +13654,7 @@ impl<'a> Parser<'a> { let ident = self.parse_identifier()?; if let GranteeName::ObjectName(namespace) = name { name = GranteeName::ObjectName(ObjectName::from(vec![Ident::new( - format!("{}:{}", namespace, ident), + format!("{namespace}:{ident}"), )])); }; } @@ -14625,7 +14624,7 @@ impl<'a> Parser<'a> { self.dialect .get_reserved_keywords_for_select_item_operator(), ) - .map(|keyword| Ident::new(format!("{:?}", keyword))); + .map(|keyword| Ident::new(format!("{keyword:?}"))); match self.parse_wildcard_expr()? { Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard( diff --git a/src/test_utils.rs b/src/test_utils.rs index 1af9f454..c7965c3f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -270,7 +270,7 @@ impl TestedDialects { tokenizer = tokenizer.with_unescape(options.unescape); } let tokens = tokenizer.tokenize().unwrap(); - assert_eq!(expected, tokens, "Tokenized differently for {:?}", dialect); + assert_eq!(expected, tokens, "Tokenized differently for {dialect:?}"); }); } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index afe1e35c..8382a534 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1751,7 +1751,7 @@ impl<'a> Tokenizer<'a> { (None, Some(tok)) => Ok(Some(tok)), (None, None) => self.tokenizer_error( chars.location(), - format!("Expected a valid binary operator after '{}'", prefix), + format!("Expected a valid binary operator after '{prefix}'"), ), } } @@ -1809,7 +1809,7 @@ impl<'a> Tokenizer<'a> { chars.next(); let mut temp = String::new(); - let end_delimiter = format!("${}$", value); + let end_delimiter = format!("${value}$"); loop { match chars.next() { @@ -2402,13 +2402,13 @@ fn take_char_from_hex_digits( location: chars.location(), })?; let digit = next_char.to_digit(16).ok_or_else(|| TokenizerError { - message: format!("Invalid hex digit in escaped unicode string: {}", next_char), + message: format!("Invalid hex digit in escaped unicode string: {next_char}"), location: chars.location(), })?; result = result * 16 + digit; } char::from_u32(result).ok_or_else(|| TokenizerError { - message: format!("Invalid unicode character: {:x}", result), + message: format!("Invalid unicode character: {result:x}"), location: chars.location(), }) } @@ -3504,7 +3504,7 @@ mod tests { } fn check_unescape(s: &str, expected: Option<&str>) { - let s = format!("'{}'", s); + let s = format!("'{s}'"); let mut state = State { peekable: s.chars().peekable(), line: 0, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index ed5d7ad2..0a60c9c4 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1345,7 +1345,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - clickhouse().verified_stmt(&format!("USE {}", object_name)), + clickhouse().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -1353,7 +1353,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - clickhouse().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + clickhouse().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -1367,7 +1367,7 @@ fn parse_use() { fn test_query_with_format_clause() { let format_options = vec!["TabSeparated", "JSONCompact", "NULL"]; for format in &format_options { - let sql = format!("SELECT * FROM t FORMAT {}", format); + let sql = format!("SELECT * FROM t FORMAT {format}"); match clickhouse_and_generic().verified_stmt(&sql) { Statement::Query(query) => { if *format == "NULL" { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 93cc89e5..6c4ed08c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3563,7 +3563,7 @@ fn test_double_value() { for (input, expected) in test_cases { for (i, expr) in input.iter().enumerate() { if let Statement::Query(query) = - dialects.one_statement_parses_to(&format!("SELECT {}", expr), "") + dialects.one_statement_parses_to(&format!("SELECT {expr}"), "") { if let SetExpr::Select(select) = *query.body { assert_eq!(expected[i], select.projection[0]); @@ -4023,13 +4023,13 @@ fn parse_create_table_column_constraint_characteristics() { syntax }; - let sql = format!("CREATE TABLE t (a int UNIQUE {})", syntax); + let sql = format!("CREATE TABLE t (a int UNIQUE {syntax})"); let expected_clause = if syntax.is_empty() { String::new() } else { format!(" {syntax}") }; - let expected = format!("CREATE TABLE t (a INT UNIQUE{})", expected_clause); + let expected = format!("CREATE TABLE t (a INT UNIQUE{expected_clause})"); let ast = one_statement_parses_to(&sql, &expected); let expected_value = if deferrable.is_some() || initially.is_some() || enforced.is_some() { @@ -7499,7 +7499,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 1)"); assert!(matches!(*query.body, SetExpr::Update(_))); } - other => panic!("Expected: UPDATE, got: {:?}", other), + other => panic!("Expected: UPDATE, got: {other:?}"), } match verified_stmt("WITH t (x) AS (SELECT 9) DELETE FROM q WHERE id IN (SELECT x FROM t)") { @@ -7507,7 +7507,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH t (x) AS (SELECT 9)"); assert!(matches!(*query.body, SetExpr::Delete(_))); } - other => panic!("Expected: DELETE, got: {:?}", other), + other => panic!("Expected: DELETE, got: {other:?}"), } match verified_stmt("WITH x AS (SELECT 42) INSERT INTO t SELECT foo FROM x") { @@ -7515,7 +7515,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 42)"); assert!(matches!(*query.body, SetExpr::Insert(_))); } - other => panic!("Expected: INSERT, got: {:?}", other), + other => panic!("Expected: INSERT, got: {other:?}"), } } @@ -10043,7 +10043,7 @@ fn parse_offset_and_limit() { #[test] fn parse_time_functions() { fn test_time_function(func_name: &'static str) { - let sql = format!("SELECT {}()", func_name); + let sql = format!("SELECT {func_name}()"); let select = verified_only_select(&sql); let select_localtime_func_call_ast = Function { name: ObjectName::from(vec![Ident::new(func_name)]), @@ -10065,7 +10065,7 @@ fn parse_time_functions() { ); // Validating Parenthesis - let sql_without_parens = format!("SELECT {}", func_name); + let sql_without_parens = format!("SELECT {func_name}"); let mut ast_without_parens = select_localtime_func_call_ast; ast_without_parens.args = FunctionArguments::None; assert_eq!( @@ -14306,7 +14306,7 @@ fn overflow() { let expr = std::iter::repeat_n("1", 1000) .collect::>() .join(" + "); - let sql = format!("SELECT {}", expr); + let sql = format!("SELECT {expr}"); let mut statements = Parser::parse_sql(&GenericDialect {}, sql.as_str()).unwrap(); let statement = statements.pop().unwrap(); @@ -14606,7 +14606,7 @@ fn test_conditional_statement_span() { else_block.unwrap().span() ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 99b7eecd..baf279fa 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -213,7 +213,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - databricks().verified_stmt(&format!("USE {}", object_name)), + databricks().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -221,7 +221,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - databricks().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + databricks().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -233,21 +233,21 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with keyword and different type of quotes assert_eq!( - databricks().verified_stmt(&format!("USE CATALOG {0}my_catalog{0}", quote)), + databricks().verified_stmt(&format!("USE CATALOG {quote}my_catalog{quote}")), Statement::Use(Use::Catalog(ObjectName::from(vec![Ident::with_quote( quote, "my_catalog".to_string(), )]))) ); assert_eq!( - databricks().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), + databricks().verified_stmt(&format!("USE DATABASE {quote}my_database{quote}")), Statement::Use(Use::Database(ObjectName::from(vec![Ident::with_quote( quote, "my_database".to_string(), )]))) ); assert_eq!( - databricks().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), + databricks().verified_stmt(&format!("USE SCHEMA {quote}my_schema{quote}")), Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote( quote, "my_schema".to_string(), @@ -357,6 +357,6 @@ fn data_type_timestamp_ntz() { }] ); } - s => panic!("Unexpected statement: {:?}", s), + s => panic!("Unexpected statement: {s:?}"), } } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index f503ed55..44cb22ce 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -368,7 +368,7 @@ fn test_duckdb_specific_int_types() { ("HUGEINT", DataType::HugeInt), ]; for (dtype_string, data_type) in duckdb_dtypes { - let sql = format!("SELECT 123::{}", dtype_string); + let sql = format!("SELECT 123::{dtype_string}"); let select = duckdb().verified_only_select(&sql); assert_eq!( &Expr::Cast { @@ -792,7 +792,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {}", object_name)), + duckdb().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -800,7 +800,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + duckdb().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -812,7 +812,9 @@ fn parse_use() { for "e in "e_styles { // Test double identifier with different type of quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), + duckdb().verified_stmt(&format!( + "USE {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Object(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index fd52b773..56a72ec8 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -524,7 +524,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - hive().verified_stmt(&format!("USE {}", object_name)), + hive().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -532,7 +532,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - hive().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + hive().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9ec28f42..ebbec25f 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1673,7 +1673,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - ms().verified_stmt(&format!("USE {}", object_name)), + ms().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -1681,7 +1681,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - ms().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + ms().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -2187,7 +2187,7 @@ fn parse_mssql_if_else() { "IF 1 = 1 BEGIN SET @A = 1; END ELSE SET @A = 2;" ); } - _ => panic!("Unexpected statements: {:?}", stmts), + _ => panic!("Unexpected statements: {stmts:?}"), } } @@ -2237,7 +2237,7 @@ fn test_mssql_if_statements_span() { Span::new(Location::new(1, 21), Location::new(1, 36)) ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Blocks @@ -2258,7 +2258,7 @@ fn test_mssql_if_statements_span() { Span::new(Location::new(1, 32), Location::new(1, 57)) ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b11d76dd..d2feee03 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -593,7 +593,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - mysql_and_generic().verified_stmt(&format!("USE {}", object_name)), + mysql_and_generic().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -601,8 +601,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - mysql_and_generic() - .verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + mysql_and_generic().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -2263,11 +2262,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { assert_eq!(&[Ident::new("t"), Ident::new("15to29")], &parts[..]); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 2: Qualified column name that starts with digits and on its own represents a number. @@ -2277,11 +2276,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { assert_eq!(&[Ident::new("t"), Ident::new("15e29")], &parts[..]); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 3: Unqualified, the same token is parsed as a number. @@ -2295,11 +2294,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { value, .. }))) => { assert_eq!(&number("15e29"), value); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 4: Quoted simple identifier. @@ -2309,11 +2308,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::Identifier(name))) => { assert_eq!(&Ident::with_quote('`', "15e29"), name); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 5: Quoted compound identifier. @@ -2326,11 +2325,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 6: Multi-level compound identifiers. @@ -2347,11 +2346,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 7: Multi-level compound quoted identifiers. @@ -2368,11 +2367,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b6605cf1..7b0a8c5d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2535,12 +2535,12 @@ fn parse_create_indices_with_operator_classes() { for expected_operator_class in &operator_classes { let single_column_sql_statement = format!( "CREATE INDEX the_index_name ON users USING {expected_index_type} (concat_users_name(first_name, last_name){})", - expected_operator_class.as_ref().map(|oc| format!(" {}", oc)) + expected_operator_class.as_ref().map(|oc| format!(" {oc}")) .unwrap_or_default() ); let multi_column_sql_statement = format!( "CREATE INDEX the_index_name ON users USING {expected_index_type} (column_name,concat_users_name(first_name, last_name){})", - expected_operator_class.as_ref().map(|oc| format!(" {}", oc)) + expected_operator_class.as_ref().map(|oc| format!(" {oc}")) .unwrap_or_default() ); @@ -3273,7 +3273,7 @@ fn test_fn_arg_with_value_operator() { assert!(matches!( &args[..], &[FunctionArg::ExprNamed { operator: FunctionArgOperator::Value, .. }] - ), "Invalid function argument: {:?}", args); + ), "Invalid function argument: {args:?}"); } other => panic!("Expected: JSON_OBJECT('name' VALUE 'value') to be parsed as a function, but got {other:?}"), } @@ -5679,7 +5679,7 @@ fn parse_drop_trigger() { "DROP TRIGGER{} check_update ON table_name{}", if if_exists { " IF EXISTS" } else { "" }, option - .map(|o| format!(" {}", o)) + .map(|o| format!(" {o}")) .unwrap_or_else(|| "".to_string()) ); assert_eq!( @@ -5773,8 +5773,7 @@ fn parse_trigger_related_functions() { // Now we parse the statements and check if they are parsed correctly. let mut statements = pg() .parse_sql_statements(&format!( - "{}{}{}{}", - sql_table_creation, sql_create_function, sql_create_trigger, sql_drop_trigger + "{sql_table_creation}{sql_create_function}{sql_create_trigger}{sql_drop_trigger}" )) .unwrap(); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 8b3988d9..3ffd3339 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2510,10 +2510,7 @@ fn test_snowflake_stage_object_names_into_location() { .zip(allowed_object_names.iter_mut()) { let (formatted_name, object_name) = it; - let sql = format!( - "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'", - formatted_name - ); + let sql = format!("COPY INTO {formatted_name} FROM 'gcs://mybucket/./../a.csv'"); match snowflake().verified_stmt(&sql) { Statement::CopyIntoSnowflake { into, .. } => { assert_eq!(into.0, object_name.0) @@ -2536,10 +2533,7 @@ fn test_snowflake_stage_object_names_into_table() { .zip(allowed_object_names.iter_mut()) { let (formatted_name, object_name) = it; - let sql = format!( - "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'", - formatted_name - ); + let sql = format!("COPY INTO {formatted_name} FROM 'gcs://mybucket/./../a.csv'"); match snowflake().verified_stmt(&sql) { Statement::CopyIntoSnowflake { into, .. } => { assert_eq!(into.0, object_name.0) @@ -3020,7 +3014,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {}", object_name)), + snowflake().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -3028,7 +3022,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + snowflake().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -3040,7 +3034,9 @@ fn parse_use() { for "e in "e_styles { // Test double identifier with different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!( + "USE {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Object(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") @@ -3059,35 +3055,37 @@ fn parse_use() { for "e in "e_styles { // Test single and double identifier with keyword and different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), + snowflake().verified_stmt(&format!("USE DATABASE {quote}my_database{quote}")), Statement::Use(Use::Database(ObjectName::from(vec![Ident::with_quote( quote, "my_database".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!("USE SCHEMA {quote}my_schema{quote}")), Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote( quote, "my_schema".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE SCHEMA {0}CATALOG{0}.{0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!( + "USE SCHEMA {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Schema(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") ]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE ROLE {0}my_role{0}", quote)), + snowflake().verified_stmt(&format!("USE ROLE {quote}my_role{quote}")), Statement::Use(Use::Role(ObjectName::from(vec![Ident::with_quote( quote, "my_role".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE WAREHOUSE {0}my_wh{0}", quote)), + snowflake().verified_stmt(&format!("USE WAREHOUSE {quote}my_wh{quote}")), Statement::Use(Use::Warehouse(ObjectName::from(vec![Ident::with_quote( quote, "my_wh".to_string(), @@ -3629,7 +3627,7 @@ fn test_alter_session_followed_by_statement() { .unwrap(); match stmts[..] { [Statement::AlterSession { .. }, Statement::Query { .. }] => {} - _ => panic!("Unexpected statements: {:?}", stmts), + _ => panic!("Unexpected statements: {stmts:?}"), } } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index b759065f..06496f0c 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -324,7 +324,7 @@ fn parse_create_table_on_conflict_col() { Keyword::IGNORE, Keyword::REPLACE, ] { - let sql = format!("CREATE TABLE t1 (a INT, b INT ON CONFLICT {:?})", keyword); + let sql = format!("CREATE TABLE t1 (a INT, b INT ON CONFLICT {keyword:?})"); match sqlite_and_generic().verified_stmt(&sql) { Statement::CreateTable(CreateTable { columns, .. }) => { assert_eq!( @@ -410,7 +410,7 @@ fn parse_window_function_with_filter() { "count", "user_defined_function", ] { - let sql = format!("SELECT {}(x) FILTER (WHERE y) OVER () FROM t", func_name); + let sql = format!("SELECT {func_name}(x) FILTER (WHERE y) OVER () FROM t"); let select = sqlite().verified_only_select(&sql); assert_eq!(select.to_string(), sql); assert_eq!( @@ -444,7 +444,7 @@ fn parse_window_function_with_filter() { fn parse_attach_database() { let sql = "ATTACH DATABASE 'test.db' AS test"; let verified_stmt = sqlite().verified_stmt(sql); - assert_eq!(sql, format!("{}", verified_stmt)); + assert_eq!(sql, format!("{verified_stmt}")); match verified_stmt { Statement::AttachDatabase { schema_name, From 6c38cdcadb45ff5b343ea49bad23fdfeebc92cf5 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:21:38 +0200 Subject: [PATCH 105/130] Snowflake: Add support for future grants (#1906) --- src/ast/mod.rs | 27 +++++++++++++++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 27 +++++++++++++++++++++++++++ tests/sqlparser_common.rs | 4 +++- 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0f682062..19966d21 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6920,6 +6920,12 @@ pub enum GrantObjects { AllSequencesInSchema { schemas: Vec }, /// Grant privileges on `ALL TABLES IN SCHEMA [, ...]` AllTablesInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE SCHEMAS IN DATABASE [, ...]` + FutureSchemasInDatabase { databases: Vec }, + /// Grant privileges on `FUTURE TABLES IN SCHEMA [, ...]` + FutureTablesInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE VIEWS IN SCHEMA [, ...]` + FutureViewsInSchema { schemas: Vec }, /// Grant privileges on specific databases Databases(Vec), /// Grant privileges on specific schemas @@ -6988,6 +6994,27 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::FutureSchemasInDatabase { databases } => { + write!( + f, + "FUTURE SCHEMAS IN DATABASE {}", + display_comma_separated(databases) + ) + } + GrantObjects::FutureTablesInSchema { schemas } => { + write!( + f, + "FUTURE TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureViewsInSchema { schemas } => { + write!( + f, + "FUTURE VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::ResourceMonitors(objects) => { write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects)) } diff --git a/src/keywords.rs b/src/keywords.rs index f56178c1..a8bbca3d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -395,6 +395,7 @@ define_keywords!( FUNCTION, FUNCTIONS, FUSION, + FUTURE, GENERAL, GENERATE, GENERATED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f1b09afb..68d89a1e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13691,6 +13691,33 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllTablesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::SCHEMAS, + Keyword::IN, + Keyword::DATABASE, + ]) { + Some(GrantObjects::FutureSchemasInDatabase { + databases: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::ALL, Keyword::SEQUENCES, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6c4ed08c..17b46e6f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9386,9 +9386,11 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); verified_stmt("GRANT EXEC ON my_sp TO runner"); verified_stmt("GRANT UPDATE ON my_table TO updater_role AS dbo"); - all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); + verified_stmt("GRANT SELECT ON FUTURE SCHEMAS IN DATABASE db1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE TABLES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); } #[test] From 50c605a47138b1602575731c196d04b0d280ad3d Mon Sep 17 00:00:00 2001 From: Sergey Olontsev Date: Sat, 28 Jun 2025 07:13:11 +0100 Subject: [PATCH 106/130] Support for Map values in ClickHouse settings (#1896) Co-authored-by: Ifeanyi Ubah --- src/ast/query.rs | 2 +- src/ast/value.rs | 1 - src/parser/mod.rs | 17 ++--- src/test_utils.rs | 5 ++ tests/sqlparser_clickhouse.rs | 130 +++++++++++++++++++++++++--------- 5 files changed, 112 insertions(+), 43 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index c79ec110..99cd2ef2 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1047,7 +1047,7 @@ impl fmt::Display for ConnectBy { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Setting { pub key: Ident, - pub value: Value, + pub value: Expr, } impl fmt::Display for Setting { diff --git a/src/ast/value.rs b/src/ast/value.rs index 90dbccbf..fdfa6a67 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -116,7 +116,6 @@ impl From for Value { derive(Visit, VisitMut), visit(with = "visit_value") )] - pub enum Value { /// Numeric literal #[cfg(not(feature = "bigdecimal"))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 68d89a1e..adf50a8f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2770,7 +2770,7 @@ impl<'a> Parser<'a> { if self.dialect.supports_dictionary_syntax() { self.prev_token(); // Put back the '{' - return self.parse_duckdb_struct_literal(); + return self.parse_dictionary(); } self.expected("an expression", token) @@ -3139,7 +3139,7 @@ impl<'a> Parser<'a> { Ok(fields) } - /// DuckDB specific: Parse a duckdb [dictionary] + /// DuckDB and ClickHouse specific: Parse a duckdb [dictionary] or a clickhouse [map] setting /// /// Syntax: /// @@ -3148,18 +3148,18 @@ impl<'a> Parser<'a> { /// ``` /// /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs - fn parse_duckdb_struct_literal(&mut self) -> Result { + /// [map]: https://clickhouse.com/docs/operations/settings/settings#additional_table_filters + fn parse_dictionary(&mut self) -> Result { self.expect_token(&Token::LBrace)?; - let fields = - self.parse_comma_separated0(Self::parse_duckdb_dictionary_field, Token::RBrace)?; + let fields = self.parse_comma_separated0(Self::parse_dictionary_field, Token::RBrace)?; self.expect_token(&Token::RBrace)?; Ok(Expr::Dictionary(fields)) } - /// Parse a field for a duckdb [dictionary] + /// Parse a field for a duckdb [dictionary] or a clickhouse [map] setting /// /// Syntax /// @@ -3168,7 +3168,8 @@ impl<'a> Parser<'a> { /// ``` /// /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs - fn parse_duckdb_dictionary_field(&mut self) -> Result { + /// [map]: https://clickhouse.com/docs/operations/settings/settings#additional_table_filters + fn parse_dictionary_field(&mut self) -> Result { let key = self.parse_identifier()?; self.expect_token(&Token::Colon)?; @@ -11216,7 +11217,7 @@ impl<'a> Parser<'a> { let key_values = self.parse_comma_separated(|p| { let key = p.parse_identifier()?; p.expect_token(&Token::Eq)?; - let value = p.parse_value()?.value; + let value = p.parse_expr()?; Ok(Setting { key, value }) })?; Some(key_values) diff --git a/src/test_utils.rs b/src/test_utils.rs index c7965c3f..db7b3dd6 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -366,6 +366,11 @@ pub fn number(n: &str) -> Value { Value::Number(n.parse().unwrap(), false) } +/// Creates a [Value::SingleQuotedString] +pub fn single_quoted_string(s: impl Into) -> Value { + Value::SingleQuotedString(s.into()) +} + pub fn table_alias(name: impl Into) -> Option { Some(TableAlias { name: Ident::new(name), diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 0a60c9c4..0288c6d2 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -28,7 +28,7 @@ use test_utils::*; use sqlparser::ast::Expr::{BinaryOp, Identifier}; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Table; -use sqlparser::ast::Value::Number; +use sqlparser::ast::Value::Boolean; use sqlparser::ast::*; use sqlparser::dialect::ClickHouseDialect; use sqlparser::dialect::GenericDialect; @@ -965,38 +965,103 @@ fn parse_limit_by() { #[test] fn parse_settings_in_query() { - match clickhouse_and_generic() - .verified_stmt(r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#) - { - Statement::Query(query) => { - assert_eq!( - query.settings, - Some(vec![ - Setting { - key: Ident::new("max_threads"), - value: Number("1".parse().unwrap(), false) - }, - Setting { - key: Ident::new("max_block_size"), - value: Number("10000".parse().unwrap(), false) - }, - ]) - ); + fn check_settings(sql: &str, expected: Vec) { + match clickhouse_and_generic().verified_stmt(sql) { + Statement::Query(q) => { + assert_eq!(q.settings, Some(expected)); + } + _ => unreachable!(), } - _ => unreachable!(), + } + + for (sql, expected_settings) in [ + ( + r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#, + vec![ + Setting { + key: Ident::new("max_threads"), + value: Expr::value(number("1")), + }, + Setting { + key: Ident::new("max_block_size"), + value: Expr::value(number("10000")), + }, + ], + ), + ( + r#"SELECT * FROM t SETTINGS additional_table_filters = {'table_1': 'x != 2'}"#, + vec![Setting { + key: Ident::new("additional_table_filters"), + value: Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "table_1"), + value: Expr::value(single_quoted_string("x != 2")).into(), + }]), + }], + ), + ( + r#"SELECT * FROM t SETTINGS additional_result_filter = 'x != 2', query_plan_optimize_lazy_materialization = false"#, + vec![ + Setting { + key: Ident::new("additional_result_filter"), + value: Expr::value(single_quoted_string("x != 2")), + }, + Setting { + key: Ident::new("query_plan_optimize_lazy_materialization"), + value: Expr::value(Boolean(false)), + }, + ], + ), + ] { + check_settings(sql, expected_settings); } let invalid_cases = vec![ - "SELECT * FROM t SETTINGS a", - "SELECT * FROM t SETTINGS a=", - "SELECT * FROM t SETTINGS a=1, b", - "SELECT * FROM t SETTINGS a=1, b=", - "SELECT * FROM t SETTINGS a=1, b=c", + ("SELECT * FROM t SETTINGS a", "Expected: =, found: EOF"), + ( + "SELECT * FROM t SETTINGS a=", + "Expected: an expression, found: EOF", + ), + ("SELECT * FROM t SETTINGS a=1, b", "Expected: =, found: EOF"), + ( + "SELECT * FROM t SETTINGS a=1, b=", + "Expected: an expression, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {", + "Expected: identifier, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b'", + "Expected: :, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': ", + "Expected: an expression, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c',}", + "Expected: identifier, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c', 'd'}", + "Expected: :, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c', 'd': }", + "Expected: an expression, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {ANY(b)}", + "Expected: :, found: (", + ), ]; - for sql in invalid_cases { - clickhouse_and_generic() - .parse_sql_statements(sql) - .expect_err("Expected: SETTINGS key = value, found: "); + for (sql, error_msg) in invalid_cases { + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError(error_msg.to_string()) + ); } } #[test] @@ -1550,11 +1615,11 @@ fn parse_select_table_function_settings() { settings: Some(vec![ Setting { key: "s0".into(), - value: Value::Number("3".parse().unwrap(), false), + value: Expr::value(number("3")), }, Setting { key: "s1".into(), - value: Value::SingleQuotedString("s".into()), + value: Expr::value(single_quoted_string("s")), }, ]), }, @@ -1575,11 +1640,11 @@ fn parse_select_table_function_settings() { settings: Some(vec![ Setting { key: "s0".into(), - value: Value::Number("3".parse().unwrap(), false), + value: Expr::value(number("3")), }, Setting { key: "s1".into(), - value: Value::SingleQuotedString("s".into()), + value: Expr::value(single_quoted_string("s")), }, ]), }, @@ -1589,7 +1654,6 @@ fn parse_select_table_function_settings() { "SELECT * FROM t(SETTINGS a=)", "SELECT * FROM t(SETTINGS a=1, b)", "SELECT * FROM t(SETTINGS a=1, b=)", - "SELECT * FROM t(SETTINGS a=1, b=c)", ]; for sql in invalid_cases { clickhouse_and_generic() From 3bc94234dfcf73d091c592edd1dab3fdd075ab82 Mon Sep 17 00:00:00 2001 From: Dima <111751109+Dimchikkk@users.noreply.github.com> Date: Sat, 28 Jun 2025 19:24:25 +0100 Subject: [PATCH 107/130] Fix join precedence for non-snowflake queries (#1905) --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 28 ++++++++++++++++++++++++++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 6 +++++- tests/sqlparser_common.rs | 23 +++++++++++++++++++++++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8f57e487..0b882247 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -52,6 +52,10 @@ impl Dialect for GenericDialect { true } + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c79b4517..028aa58a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -278,6 +278,34 @@ pub trait Dialect: Debug + Any { false } + /// Indicates whether the dialect supports left-associative join parsing + /// by default when parentheses are omitted in nested joins. + /// + /// Most dialects (like MySQL or Postgres) assume **left-associative** precedence, + /// so a query like: + /// + /// ```sql + /// SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON ... + /// ``` + /// is interpreted as: + /// ```sql + /// ((t1 NATURAL JOIN t5) INNER JOIN t0 ON ...) + /// ``` + /// and internally represented as a **flat list** of joins. + /// + /// In contrast, some dialects (e.g. **Snowflake**) assume **right-associative** + /// precedence and interpret the same query as: + /// ```sql + /// (t1 NATURAL JOIN (t5 INNER JOIN t0 ON ...)) + /// ``` + /// which results in a **nested join** structure in the AST. + /// + /// If this method returns `false`, the parser must build nested join trees + /// even in the absence of parentheses to reflect the correct associativity + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + /// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN. fn supports_outer_join_operator(&self) -> bool { false diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 5ebb7e37..ba28a8ec 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -283,6 +283,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_left_associative_joins_without_parens(&self) -> bool { + false + } + fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { // Unreserve some keywords that Snowflake accepts as identifiers // See: https://docs.snowflake.com/en/sql-reference/reserved-keywords diff --git a/src/parser/mod.rs b/src/parser/mod.rs index adf50a8f..a079488f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12495,7 +12495,11 @@ impl<'a> Parser<'a> { }; let mut relation = self.parse_table_factor()?; - if self.peek_parens_less_nested_join() { + if !self + .dialect + .supports_left_associative_joins_without_parens() + && self.peek_parens_less_nested_join() + { let joins = self.parse_joins()?; relation = TableFactor::NestedJoin { table_with_joins: Box::new(TableWithJoins { relation, joins }), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 17b46e6f..f6f51459 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15359,6 +15359,29 @@ fn check_enforced() { ); } +#[test] +fn join_precedence() { + all_dialects_except(|d| !d.supports_left_associative_joins_without_parens()) + .verified_query_with_canonical( + "SELECT * + FROM t1 + NATURAL JOIN t5 + INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 + WHERE t0.v1 = t1.v0", + // canonical string without parentheses + "SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 WHERE t0.v1 = t1.v0", + ); + all_dialects_except(|d| d.supports_left_associative_joins_without_parens()).verified_query_with_canonical( + "SELECT * + FROM t1 + NATURAL JOIN t5 + INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 + WHERE t0.v1 = t1.v0", + // canonical string with parentheses + "SELECT * FROM t1 NATURAL JOIN (t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0) WHERE t0.v1 = t1.v0", + ); +} + #[test] fn parse_create_procedure_with_language() { let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#; From abd80f9ecbf11c9d4b533a59010f079cd9df5b6c Mon Sep 17 00:00:00 2001 From: Simon Vandel Sillesen Date: Mon, 30 Jun 2025 17:51:55 +0200 Subject: [PATCH 108/130] Support remaining pipe operators (#1879) --- src/ast/query.rs | 153 ++++++++++++++ src/parser/mod.rs | 170 ++++++++++++++++ tests/sqlparser_common.rs | 416 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 739 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index 99cd2ef2..febf1fc6 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2684,6 +2684,79 @@ pub enum PipeOperator { /// Syntax: `|> TABLESAMPLE SYSTEM (10 PERCENT) /// See more at TableSample { sample: Box }, + /// Renames columns in the input table. + /// + /// Syntax: `|> RENAME old_name AS new_name, ...` + /// + /// See more at + Rename { mappings: Vec }, + /// Combines the input table with one or more tables using UNION. + /// + /// Syntax: `|> UNION [ALL|DISTINCT] (), (), ...` + /// + /// See more at + Union { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Returns only the rows that are present in both the input table and the specified tables. + /// + /// Syntax: `|> INTERSECT [DISTINCT] (), (), ...` + /// + /// See more at + Intersect { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Returns only the rows that are present in the input table but not in the specified tables. + /// + /// Syntax: `|> EXCEPT DISTINCT (), (), ...` + /// + /// See more at + Except { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Calls a table function or procedure that returns a table. + /// + /// Syntax: `|> CALL function_name(args) [AS alias]` + /// + /// See more at + Call { + function: Function, + alias: Option, + }, + /// Pivots data from rows to columns. + /// + /// Syntax: `|> PIVOT(aggregate_function(column) FOR pivot_column IN (value1, value2, ...)) [AS alias]` + /// + /// See more at + Pivot { + aggregate_functions: Vec, + value_column: Vec, + value_source: PivotValueSource, + alias: Option, + }, + /// The `UNPIVOT` pipe operator transforms columns into rows. + /// + /// Syntax: + /// ```sql + /// |> UNPIVOT(value_column FOR name_column IN (column1, column2, ...)) [alias] + /// ``` + /// + /// See more at + Unpivot { + value_column: Ident, + name_column: Ident, + unpivot_columns: Vec, + alias: Option, + }, + /// Joins the input table with another table. + /// + /// Syntax: `|> [JOIN_TYPE] JOIN
[alias] ON ` or `|> [JOIN_TYPE] JOIN
[alias] USING ()` + /// + /// See more at + Join(Join), } impl fmt::Display for PipeOperator { @@ -2739,10 +2812,90 @@ impl fmt::Display for PipeOperator { PipeOperator::TableSample { sample } => { write!(f, "{sample}") } + PipeOperator::Rename { mappings } => { + write!(f, "RENAME {}", display_comma_separated(mappings)) + } + PipeOperator::Union { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "UNION", set_quantifier, queries), + PipeOperator::Intersect { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "INTERSECT", set_quantifier, queries), + PipeOperator::Except { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "EXCEPT", set_quantifier, queries), + PipeOperator::Call { function, alias } => { + write!(f, "CALL {function}")?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Pivot { + aggregate_functions, + value_column, + value_source, + alias, + } => { + write!( + f, + "PIVOT({} FOR {} IN ({}))", + display_comma_separated(aggregate_functions), + Expr::CompoundIdentifier(value_column.to_vec()), + value_source + )?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Unpivot { + value_column, + name_column, + unpivot_columns, + alias, + } => { + write!( + f, + "UNPIVOT({} FOR {} IN ({}))", + value_column, + name_column, + display_comma_separated(unpivot_columns) + )?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Join(join) => write!(f, "{join}"), } } } +impl PipeOperator { + /// Helper function to format optional alias for pipe operators + fn fmt_optional_alias(f: &mut fmt::Formatter<'_>, alias: &Option) -> fmt::Result { + if let Some(alias) = alias { + write!(f, " AS {alias}")?; + } + Ok(()) + } + + /// Helper function to format set operations (UNION, INTERSECT, EXCEPT) with queries + fn fmt_set_operation( + f: &mut fmt::Formatter<'_>, + operation: &str, + set_quantifier: &SetQuantifier, + queries: &[Query], + ) -> fmt::Result { + write!(f, "{operation}")?; + match set_quantifier { + SetQuantifier::None => {} + _ => { + write!(f, " {set_quantifier}")?; + } + } + write!(f, " ")?; + let parenthesized_queries: Vec = + queries.iter().map(|query| format!("({query})")).collect(); + write!(f, "{}", display_comma_separated(&parenthesized_queries)) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a079488f..4f8f1b85 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9988,6 +9988,48 @@ impl<'a> Parser<'a> { Ok(IdentWithAlias { ident, alias }) } + /// Parse `identifier [AS] identifier` where the AS keyword is optional + fn parse_identifier_with_optional_alias(&mut self) -> Result { + let ident = self.parse_identifier()?; + let _after_as = self.parse_keyword(Keyword::AS); + let alias = self.parse_identifier()?; + Ok(IdentWithAlias { ident, alias }) + } + + /// Parse comma-separated list of parenthesized queries for pipe operators + fn parse_pipe_operator_queries(&mut self) -> Result, ParserError> { + self.parse_comma_separated(|parser| { + parser.expect_token(&Token::LParen)?; + let query = parser.parse_query()?; + parser.expect_token(&Token::RParen)?; + Ok(*query) + }) + } + + /// Parse set quantifier for pipe operators that require DISTINCT. E.g. INTERSECT and EXCEPT + fn parse_distinct_required_set_quantifier( + &mut self, + operator_name: &str, + ) -> Result { + let quantifier = self.parse_set_quantifier(&Some(SetOperator::Intersect)); + match quantifier { + SetQuantifier::Distinct | SetQuantifier::DistinctByName => Ok(quantifier), + _ => Err(ParserError::ParserError(format!( + "{operator_name} pipe operator requires DISTINCT modifier", + ))), + } + } + + /// Parse optional identifier alias (with or without AS keyword) + fn parse_identifier_optional_alias(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::AS) { + Ok(Some(self.parse_identifier()?)) + } else { + // Check if the next token is an identifier (implicit alias) + self.maybe_parse(|parser| parser.parse_identifier()) + } + } + /// Optionally parses an alias for a select list item fn maybe_parse_select_item_alias(&mut self) -> Result, ParserError> { fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { @@ -11134,6 +11176,19 @@ impl<'a> Parser<'a> { Keyword::AGGREGATE, Keyword::ORDER, Keyword::TABLESAMPLE, + Keyword::RENAME, + Keyword::UNION, + Keyword::INTERSECT, + Keyword::EXCEPT, + Keyword::CALL, + Keyword::PIVOT, + Keyword::UNPIVOT, + Keyword::JOIN, + Keyword::INNER, + Keyword::LEFT, + Keyword::RIGHT, + Keyword::FULL, + Keyword::CROSS, ])?; match kw { Keyword::SELECT => { @@ -11200,6 +11255,121 @@ impl<'a> Parser<'a> { let sample = self.parse_table_sample(TableSampleModifier::TableSample)?; pipe_operators.push(PipeOperator::TableSample { sample }); } + Keyword::RENAME => { + let mappings = + self.parse_comma_separated(Parser::parse_identifier_with_optional_alias)?; + pipe_operators.push(PipeOperator::Rename { mappings }); + } + Keyword::UNION => { + let set_quantifier = self.parse_set_quantifier(&Some(SetOperator::Union)); + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Union { + set_quantifier, + queries, + }); + } + Keyword::INTERSECT => { + let set_quantifier = + self.parse_distinct_required_set_quantifier("INTERSECT")?; + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Intersect { + set_quantifier, + queries, + }); + } + Keyword::EXCEPT => { + let set_quantifier = self.parse_distinct_required_set_quantifier("EXCEPT")?; + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Except { + set_quantifier, + queries, + }); + } + Keyword::CALL => { + let function_name = self.parse_object_name(false)?; + let function_expr = self.parse_function(function_name)?; + if let Expr::Function(function) = function_expr { + let alias = self.parse_identifier_optional_alias()?; + pipe_operators.push(PipeOperator::Call { function, alias }); + } else { + return Err(ParserError::ParserError( + "Expected function call after CALL".to_string(), + )); + } + } + Keyword::PIVOT => { + self.expect_token(&Token::LParen)?; + let aggregate_functions = + self.parse_comma_separated(Self::parse_aliased_function_call)?; + self.expect_keyword_is(Keyword::FOR)?; + let value_column = self.parse_period_separated(|p| p.parse_identifier())?; + self.expect_keyword_is(Keyword::IN)?; + + self.expect_token(&Token::LParen)?; + let value_source = if self.parse_keyword(Keyword::ANY) { + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_order_by_expr)? + } else { + vec![] + }; + PivotValueSource::Any(order_by) + } else if self.peek_sub_query() { + PivotValueSource::Subquery(self.parse_query()?) + } else { + PivotValueSource::List( + self.parse_comma_separated(Self::parse_expr_with_alias)?, + ) + }; + self.expect_token(&Token::RParen)?; + self.expect_token(&Token::RParen)?; + + let alias = self.parse_identifier_optional_alias()?; + + pipe_operators.push(PipeOperator::Pivot { + aggregate_functions, + value_column, + value_source, + alias, + }); + } + Keyword::UNPIVOT => { + self.expect_token(&Token::LParen)?; + let value_column = self.parse_identifier()?; + self.expect_keyword(Keyword::FOR)?; + let name_column = self.parse_identifier()?; + self.expect_keyword(Keyword::IN)?; + + self.expect_token(&Token::LParen)?; + let unpivot_columns = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + + self.expect_token(&Token::RParen)?; + + let alias = self.parse_identifier_optional_alias()?; + + pipe_operators.push(PipeOperator::Unpivot { + value_column, + name_column, + unpivot_columns, + alias, + }); + } + Keyword::JOIN + | Keyword::INNER + | Keyword::LEFT + | Keyword::RIGHT + | Keyword::FULL + | Keyword::CROSS => { + self.prev_token(); + let mut joins = self.parse_joins()?; + if joins.len() != 1 { + return Err(ParserError::ParserError( + "Join pipe operator must have a single join".to_string(), + )); + } + let join = joins.swap_remove(0); + pipe_operators.push(PipeOperator::Join(join)) + } unhandled => { return Err(ParserError::ParserError(format!( "`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}" diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f6f51459..42bdeae1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15217,10 +15217,426 @@ fn parse_pipeline_operator() { dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50 PERCENT)"); dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50) REPEATABLE (10)"); + // rename pipe operator + dialects.verified_stmt("SELECT * FROM users |> RENAME old_name AS new_name"); + dialects.verified_stmt("SELECT * FROM users |> RENAME id AS user_id, name AS user_name"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> RENAME id user_id", + "SELECT * FROM users |> RENAME id AS user_id", + ); + + // union pipe operator + dialects.verified_stmt("SELECT * FROM users |> UNION ALL (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION (SELECT * FROM admins)"); + + // union pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> UNION ALL (SELECT * FROM admins), (SELECT * FROM guests)", + ); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins), (SELECT * FROM guests), (SELECT * FROM employees)"); + dialects.verified_stmt( + "SELECT * FROM users |> UNION (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // union pipe operator with BY NAME modifier + dialects.verified_stmt("SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION ALL BY NAME (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT BY NAME (SELECT * FROM admins)"); + + // union pipe operator with BY NAME and multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // intersect pipe operator (BigQuery requires DISTINCT modifier for INTERSECT) + dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins)"); + + // intersect pipe operator with BY NAME modifier + dialects + .verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins)"); + + // intersect pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // intersect pipe operator with BY NAME and multiple queries + dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)"); + + // except pipe operator (BigQuery requires DISTINCT modifier for EXCEPT) + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins)"); + + // except pipe operator with BY NAME modifier + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins)"); + + // except pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // except pipe operator with BY NAME and multiple queries + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)"); + + // call pipe operator + dialects.verified_stmt("SELECT * FROM users |> CALL my_function()"); + dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5, 'test')"); + dialects.verified_stmt( + "SELECT * FROM users |> CALL namespace.function_name(col1, col2, 'literal')", + ); + + // call pipe operator with complex arguments + dialects.verified_stmt("SELECT * FROM users |> CALL transform_data(col1 + col2)"); + dialects.verified_stmt("SELECT * FROM users |> CALL analyze_data('param1', 100, true)"); + + // call pipe operator with aliases + dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) AS al"); + dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5) AS result_table"); + dialects.verified_stmt("SELECT * FROM users |> CALL namespace.func() AS my_alias"); + + // multiple call pipe operators in sequence + dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) |> CALL tvf2(arg2, arg3)"); + dialects.verified_stmt( + "SELECT * FROM data |> CALL transform(col1) |> CALL validate() |> CALL process(param)", + ); + + // multiple call pipe operators with aliases + dialects.verified_stmt( + "SELECT * FROM input_table |> CALL tvf1(arg1) AS step1 |> CALL tvf2(arg2) AS step2", + ); + dialects.verified_stmt( + "SELECT * FROM data |> CALL preprocess() AS clean_data |> CALL analyze(mode) AS results", + ); + + // call pipe operators mixed with other pipe operators + dialects.verified_stmt( + "SELECT * FROM users |> CALL transform() |> WHERE status = 'active' |> CALL process(param)", + ); + dialects.verified_stmt( + "SELECT * FROM data |> CALL preprocess() AS clean |> SELECT col1, col2 |> CALL validate()", + ); + + // pivot pipe operator + dialects.verified_stmt( + "SELECT * FROM monthly_sales |> PIVOT(SUM(amount) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))", + ); + dialects.verified_stmt("SELECT * FROM sales_data |> PIVOT(AVG(revenue) FOR region IN ('North', 'South', 'East', 'West'))"); + + // pivot pipe operator with multiple aggregate functions + dialects.verified_stmt("SELECT * FROM data |> PIVOT(SUM(sales) AS total_sales, COUNT(*) AS num_transactions FOR month IN ('Jan', 'Feb', 'Mar'))"); + + // pivot pipe operator with compound column names + dialects.verified_stmt("SELECT * FROM sales |> PIVOT(SUM(amount) FOR product.category IN ('Electronics', 'Clothing'))"); + + // pivot pipe operator mixed with other pipe operators + dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> PIVOT(SUM(revenue) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))"); + + // pivot pipe operator with aliases + dialects.verified_stmt("SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales"); + dialects.verified_stmt("SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category"); + dialects.verified_stmt("SELECT * FROM sales |> PIVOT(COUNT(*) AS transactions, SUM(amount) AS total FOR region IN ('North', 'South')) AS regional_summary"); + + // pivot pipe operator with implicit aliases (without AS keyword) + dialects.verified_query_with_canonical( + "SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) quarterly_sales", + "SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) avg_by_category", + "SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category", + ); + + // unpivot pipe operator basic usage + dialects + .verified_stmt("SELECT * FROM sales |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))"); + dialects.verified_stmt("SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C))"); + dialects.verified_stmt( + "SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory, disk))", + ); + + // unpivot pipe operator with multiple columns + dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (jan, feb, mar, apr, may, jun))"); + dialects.verified_stmt( + "SELECT * FROM report |> UNPIVOT(score FOR subject IN (math, science, english, history))", + ); + + // unpivot pipe operator mixed with other pipe operators + dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))"); + + // unpivot pipe operator with aliases + dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales"); + dialects.verified_stmt( + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data", + ); + dialects.verified_stmt("SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory)) AS metric_measurements"); + + // unpivot pipe operator with implicit aliases (without AS keyword) + dialects.verified_query_with_canonical( + "SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) unpivoted_sales", + "SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) transformed_data", + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data", + ); + // many pipes dialects.verified_stmt( "SELECT * FROM CustomerOrders |> AGGREGATE SUM(cost) AS total_cost GROUP BY customer_id, state, item_type |> EXTEND COUNT(*) OVER (PARTITION BY customer_id) AS num_orders |> WHERE num_orders > 1 |> AGGREGATE AVG(total_cost) AS average GROUP BY state DESC, item_type ASC", ); + + // join pipe operator - INNER JOIN + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt("SELECT * FROM users |> INNER JOIN orders ON users.id = orders.user_id"); + + // join pipe operator - LEFT JOIN + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt( + "SELECT * FROM users |> LEFT OUTER JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - RIGHT JOIN + dialects.verified_stmt("SELECT * FROM users |> RIGHT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt( + "SELECT * FROM users |> RIGHT OUTER JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - FULL JOIN + dialects.verified_stmt("SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> FULL OUTER JOIN orders ON users.id = orders.user_id", + "SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - CROSS JOIN + dialects.verified_stmt("SELECT * FROM users |> CROSS JOIN orders"); + + // join pipe operator with USING + dialects.verified_query_with_canonical( + "SELECT * FROM users |> JOIN orders USING (user_id)", + "SELECT * FROM users |> JOIN orders USING(user_id)", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> LEFT JOIN orders USING (user_id, order_date)", + "SELECT * FROM users |> LEFT JOIN orders USING(user_id, order_date)", + ); + + // join pipe operator with alias + dialects.verified_query_with_canonical( + "SELECT * FROM users |> JOIN orders o ON users.id = o.user_id", + "SELECT * FROM users |> JOIN orders AS o ON users.id = o.user_id", + ); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders AS o ON users.id = o.user_id"); + + // join pipe operator with complex ON condition + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id AND orders.status = 'active'"); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id AND orders.amount > 100"); + + // multiple join pipe operators + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> JOIN products ON orders.product_id = products.id"); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id |> RIGHT JOIN products ON orders.product_id = products.id"); + + // join pipe operator with other pipe operators + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> WHERE orders.amount > 100"); + dialects.verified_stmt("SELECT * FROM users |> WHERE users.active = true |> LEFT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> SELECT users.name, orders.amount"); +} + +#[test] +fn parse_pipeline_operator_negative_tests() { + let dialects = all_dialects_where(|d| d.supports_pipe_operator()); + + // Test that plain EXCEPT without DISTINCT fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT ALL fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT ALL (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT BY NAME without DISTINCT fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT BY NAME (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT ALL BY NAME fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements( + "SELECT * FROM users |> EXCEPT ALL BY NAME (SELECT * FROM admins)" + ) + .unwrap_err() + ); + + // Test that plain INTERSECT without DISTINCT fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT ALL fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT ALL (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT BY NAME without DISTINCT fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT BY NAME (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT ALL BY NAME fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements( + "SELECT * FROM users |> INTERSECT ALL BY NAME (SELECT * FROM admins)" + ) + .unwrap_err() + ); + + // Test that CALL without function name fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL") + .is_err()); + + // Test that CALL without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function") + .is_err()); + + // Test that CALL with invalid function syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL 123invalid") + .is_err()); + + // Test that CALL with malformed arguments fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function(,)") + .is_err()); + + // Test that CALL with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function() AS") + .is_err()); + + // Test that PIVOT without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT SUM(amount) FOR month IN ('Jan')") + .is_err()); + + // Test that PIVOT without FOR keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) month IN ('Jan'))") + .is_err()); + + // Test that PIVOT without IN keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month ('Jan'))") + .is_err()); + + // Test that PIVOT with empty IN list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ())") + .is_err()); + + // Test that PIVOT with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ('Jan')) AS") + .is_err()); + + // Test UNPIVOT negative cases + + // Test that UNPIVOT without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT value FOR name IN col1, col2") + .is_err()); + + // Test that UNPIVOT without FOR keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value name IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT without IN keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name (col1, col2))") + .is_err()); + + // Test that UNPIVOT with missing value column fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(FOR name IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT with missing name column fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT with empty IN list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN ())") + .is_err()); + + // Test that UNPIVOT with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)) AS") + .is_err()); + + // Test that UNPIVOT with missing closing parenthesis fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)") + .is_err()); + + // Test that JOIN without table name fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN ON users.id = orders.user_id") + .is_err()); + + // Test that CROSS JOIN with ON condition fails + assert!(dialects + .parse_sql_statements( + "SELECT * FROM users |> CROSS JOIN orders ON users.id = orders.user_id" + ) + .is_err()); + + // Test that CROSS JOIN with USING condition fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CROSS JOIN orders USING (user_id)") + .is_err()); + + // Test that JOIN with empty USING list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders USING ()") + .is_err()); + + // Test that JOIN with malformed ON condition fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders ON") + .is_err()); + + // Test that JOIN with invalid USING syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders USING user_id") + .is_err()); } #[test] From 9ffc546870a46eebb96a64e127b2190a05fa2baf Mon Sep 17 00:00:00 2001 From: Simon Vandel Sillesen Date: Tue, 1 Jul 2025 13:19:40 +0200 Subject: [PATCH 109/130] Make `GenericDialect` support from-first syntax (#1911) --- src/dialect/generic.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 0b882247..59671e21 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -112,6 +112,10 @@ impl Dialect for GenericDialect { true } + fn supports_from_first_select(&self) -> bool { + true + } + fn supports_asc_desc_in_column_definition(&self) -> bool { true } From f32a41a00451888fea24ee3487e303c85b8204b3 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:52:29 +0200 Subject: [PATCH 110/130] Redshift utf8 idents (#1915) --- src/dialect/redshift.rs | 10 ++++++---- tests/sqlparser_common.rs | 9 ++++++++- tests/sqlparser_redshift.rs | 5 +++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index feccca5d..9ad9c5fd 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -80,13 +80,15 @@ impl Dialect for RedshiftSqlDialect { } fn is_identifier_start(&self, ch: char) -> bool { - // Extends Postgres dialect with sharp - PostgreSqlDialect {}.is_identifier_start(ch) || ch == '#' + // Extends Postgres dialect with sharp and UTF-8 multibyte chars + // https://docs.aws.amazon.com/redshift/latest/dg/r_names.html + PostgreSqlDialect {}.is_identifier_start(ch) || ch == '#' || !ch.is_ascii() } fn is_identifier_part(&self, ch: char) -> bool { - // Extends Postgres dialect with sharp - PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#' + // Extends Postgres dialect with sharp and UTF-8 multibyte chars + // https://docs.aws.amazon.com/redshift/latest/dg/r_names.html + PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#' || !ch.is_ascii() } /// redshift has `CONVERT(type, value)` instead of `CONVERT(value, type)` diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 42bdeae1..10a79252 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11068,10 +11068,17 @@ fn parse_non_latin_identifiers() { Box::new(RedshiftSqlDialect {}), Box::new(MySqlDialect {}), ]); - supported_dialects.verified_stmt("SELECT a.説明 FROM test.public.inter01 AS a"); supported_dialects.verified_stmt("SELECT a.説明 FROM inter01 AS a, inter01_transactions AS b WHERE a.説明 = b.取引 GROUP BY a.説明"); supported_dialects.verified_stmt("SELECT 説明, hühnervögel, garçon, Москва, 東京 FROM inter01"); + + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(MySqlDialect {}), + ]); assert!(supported_dialects .parse_sql_statements("SELECT 💝 FROM table1") .is_err()); diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index be2b6722..d539adf6 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -402,3 +402,8 @@ fn parse_extract_single_quotes() { fn parse_string_literal_backslash_escape() { redshift().one_statement_parses_to(r#"SELECT 'l\'auto'"#, "SELECT 'l''auto'"); } + +#[test] +fn parse_utf8_multibyte_idents() { + redshift().verified_stmt("SELECT 🚀.city AS 🎸 FROM customers AS 🚀"); +} From a3398223d7d6fcc0e5329aec76ceef040aeabaa9 Mon Sep 17 00:00:00 2001 From: Ryan Schneider Date: Wed, 2 Jul 2025 05:57:08 -0700 Subject: [PATCH 111/130] DuckDB: Add support for multiple `TRIM` arguments (#1916) Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 1 - tests/sqlparser_duckdb.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4f8f1b85..a3856d70 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2576,7 +2576,7 @@ impl<'a> Parser<'a> { trim_characters: None, }) } else if self.consume_token(&Token::Comma) - && dialect_of!(self is SnowflakeDialect | BigQueryDialect | GenericDialect) + && dialect_of!(self is DuckDbDialect | SnowflakeDialect | BigQueryDialect | GenericDialect) { 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 10a79252..61a4de40 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7762,7 +7762,6 @@ fn parse_trim() { Box::new(MySqlDialect {}), //Box::new(BigQueryDialect {}), Box::new(SQLiteDialect {}), - Box::new(DuckDbDialect {}), ]); assert_eq!( diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 44cb22ce..371d3aac 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -24,6 +24,7 @@ use test_utils::*; use sqlparser::ast::*; use sqlparser::dialect::{DuckDbDialect, GenericDialect}; +use sqlparser::parser::ParserError; fn duckdb() -> TestedDialects { TestedDialects::new(vec![Box::new(DuckDbDialect {})]) @@ -830,3 +831,32 @@ fn parse_use() { ]))) ); } + +#[test] +fn test_duckdb_trim() { + let real_sql = r#"SELECT customer_id, TRIM(item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; + assert_eq!(duckdb().verified_stmt(real_sql).to_string(), real_sql); + + let sql_only_select = "SELECT TRIM('xyz', 'a')"; + let select = duckdb().verified_only_select(sql_only_select); + 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)) + ); + + // missing comma separation + let error_sql = "SELECT TRIM('xyz' 'a')"; + assert_eq!( + ParserError::ParserError("Expected: ), found: 'a'".to_owned()), + duckdb().parse_sql_statements(error_sql).unwrap_err() + ); +} From 015caca611f67c22d7e1da5df11a5313afd97509 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:16:21 +0200 Subject: [PATCH 112/130] Redshift alter column type no set (#1912) --- src/ast/ddl.rs | 20 +++++++++++++++----- src/ast/spans.rs | 1 + src/dialect/mod.rs | 9 +++++++++ src/dialect/postgresql.rs | 4 ++++ src/parser/mod.rs | 30 ++++++++++++++++++++---------- tests/sqlparser_common.rs | 17 ++++++++--------- tests/sqlparser_postgres.rs | 6 ++---- 7 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index afe9f228..9134412c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -893,7 +893,10 @@ pub enum AlterColumnOperation { data_type: DataType, /// PostgreSQL specific using: Option, + /// Set to true if the statement includes the `SET DATA TYPE` keywords + had_set: bool, }, + /// `ADD GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ]` /// /// Note: this is a PostgreSQL-specific operation. @@ -914,12 +917,19 @@ impl fmt::Display for AlterColumnOperation { AlterColumnOperation::DropDefault => { write!(f, "DROP DEFAULT") } - AlterColumnOperation::SetDataType { data_type, using } => { - if let Some(expr) = using { - write!(f, "SET DATA TYPE {data_type} USING {expr}") - } else { - write!(f, "SET DATA TYPE {data_type}") + AlterColumnOperation::SetDataType { + data_type, + using, + had_set, + } => { + if *had_set { + write!(f, "SET DATA ")?; } + write!(f, "TYPE {data_type}")?; + if let Some(expr) = using { + write!(f, " USING {expr}")?; + } + Ok(()) } AlterColumnOperation::AddGenerated { generated_as, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 78ed772b..00882602 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -924,6 +924,7 @@ impl Spanned for AlterColumnOperation { AlterColumnOperation::SetDataType { data_type: _, using, + had_set: _, } => using.as_ref().map_or(Span::empty(), |u| u.span()), AlterColumnOperation::AddGenerated { .. } => Span::empty(), } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 028aa58a..b214699c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1060,6 +1060,15 @@ pub trait Dialect: Debug + Any { fn supports_space_separated_column_options(&self) -> bool { false } + + /// Returns true if the dialect supports the `USING` clause in an `ALTER COLUMN` statement. + /// Example: + /// ```sql + /// ALTER TABLE tbl ALTER COLUMN col SET DATA TYPE USING ` + /// ``` + fn supports_alter_column_type_using(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index a91ab598..b2d4014c 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -258,4 +258,8 @@ impl Dialect for PostgreSqlDialect { fn supports_set_names(&self) -> bool { true } + + fn supports_alter_column_type_using(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a3856d70..6360817f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8734,16 +8734,10 @@ impl<'a> Parser<'a> { } } else if self.parse_keywords(&[Keyword::DROP, Keyword::DEFAULT]) { AlterColumnOperation::DropDefault {} - } else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE]) - || (is_postgresql && self.parse_keyword(Keyword::TYPE)) - { - let data_type = self.parse_data_type()?; - let using = if is_postgresql && self.parse_keyword(Keyword::USING) { - Some(self.parse_expr()?) - } else { - None - }; - AlterColumnOperation::SetDataType { data_type, using } + } else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE]) { + self.parse_set_data_type(true)? + } else if self.parse_keyword(Keyword::TYPE) { + self.parse_set_data_type(false)? } else if self.parse_keywords(&[Keyword::ADD, Keyword::GENERATED]) { let generated_as = if self.parse_keyword(Keyword::ALWAYS) { Some(GeneratedAs::Always) @@ -8909,6 +8903,22 @@ impl<'a> Parser<'a> { Ok(operation) } + fn parse_set_data_type(&mut self, had_set: bool) -> Result { + let data_type = self.parse_data_type()?; + let using = if self.dialect.supports_alter_column_type_using() + && self.parse_keyword(Keyword::USING) + { + Some(self.parse_expr()?) + } else { + None + }; + Ok(AlterColumnOperation::SetDataType { + data_type, + using, + had_set, + }) + } + fn parse_part_or_partition(&mut self) -> Result { let keyword = self.expect_one_of_keywords(&[Keyword::PART, Keyword::PARTITION])?; match keyword { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 61a4de40..380bd475 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5057,22 +5057,21 @@ fn parse_alter_table_alter_column_type() { AlterColumnOperation::SetDataType { data_type: DataType::Text, using: None, + had_set: true, } ); } _ => unreachable!(), } + verified_stmt(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT")); - let dialect = TestedDialects::new(vec![Box::new(GenericDialect {})]); + let dialects = all_dialects_where(|d| d.supports_alter_column_type_using()); + dialects.verified_stmt(&format!( + "{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'" + )); - let res = - dialect.parse_sql_statements(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT")); - assert_eq!( - ParserError::ParserError("Expected: SET/DROP NOT NULL, SET DEFAULT, or SET DATA TYPE after ALTER COLUMN, found: TYPE".to_string()), - res.unwrap_err() - ); - - let res = dialect.parse_sql_statements(&format!( + let dialects = all_dialects_except(|d| d.supports_alter_column_type_using()); + let res = dialects.parse_sql_statements(&format!( "{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'" )); assert_eq!( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7b0a8c5d..e1a49c69 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -764,10 +764,7 @@ fn parse_drop_extension() { #[test] fn parse_alter_table_alter_column() { - pg().one_statement_parses_to( - "ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'", - "ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'", - ); + pg().verified_stmt("ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'"); match alter_table_op( pg().verified_stmt( @@ -783,6 +780,7 @@ fn parse_alter_table_alter_column() { AlterColumnOperation::SetDataType { data_type: DataType::Text, using: Some(using_expr), + had_set: true, } ); } From 418b94227a5cf6ae721629f95a279c40b6a196ad Mon Sep 17 00:00:00 2001 From: carl <44021312+achristmascarl@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:19:26 -0400 Subject: [PATCH 113/130] Postgres: support `ADD CONSTRAINT NOT VALID` and `VALIDATE CONSTRAINT` (#1908) --- src/ast/ddl.rs | 25 +++++++++++++++++--- src/ast/spans.rs | 6 ++++- src/keywords.rs | 1 + src/parser/mod.rs | 9 ++++++- src/test_utils.rs | 31 ++++++++++++++---------- tests/sqlparser_common.rs | 2 +- tests/sqlparser_postgres.rs | 47 ++++++++++++++++++++++++++++++++++--- 7 files changed, 99 insertions(+), 22 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9134412c..9d500203 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -67,8 +67,11 @@ impl fmt::Display for ReplicaIdentity { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum AlterTableOperation { - /// `ADD ` - AddConstraint(TableConstraint), + /// `ADD [NOT VALID]` + AddConstraint { + constraint: TableConstraint, + not_valid: bool, + }, /// `ADD [COLUMN] [IF NOT EXISTS] ` AddColumn { /// `[COLUMN]`. @@ -344,6 +347,10 @@ pub enum AlterTableOperation { equals: bool, value: ValueWithSpan, }, + /// `VALIDATE CONSTRAINT ` + ValidateConstraint { + name: Ident, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -494,7 +501,16 @@ impl fmt::Display for AlterTableOperation { display_separated(new_partitions, " "), ine = if *if_not_exists { " IF NOT EXISTS" } else { "" } ), - AlterTableOperation::AddConstraint(c) => write!(f, "ADD {c}"), + AlterTableOperation::AddConstraint { + not_valid, + constraint, + } => { + write!(f, "ADD {constraint}")?; + if *not_valid { + write!(f, " NOT VALID")?; + } + Ok(()) + } AlterTableOperation::AddColumn { column_keyword, if_not_exists, @@ -772,6 +788,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::ReplicaIdentity { identity } => { write!(f, "REPLICA IDENTITY {identity}") } + AlterTableOperation::ValidateConstraint { name } => { + write!(f, "VALIDATE CONSTRAINT {name}") + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 00882602..26205496 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1075,7 +1075,10 @@ impl Spanned for CreateTableOptions { impl Spanned for AlterTableOperation { fn span(&self) -> Span { match self { - AlterTableOperation::AddConstraint(table_constraint) => table_constraint.span(), + AlterTableOperation::AddConstraint { + constraint, + not_valid: _, + } => constraint.span(), AlterTableOperation::AddColumn { column_keyword: _, if_not_exists: _, @@ -1196,6 +1199,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::AutoIncrement { value, .. } => value.span(), AlterTableOperation::Lock { .. } => Span::empty(), AlterTableOperation::ReplicaIdentity { .. } => Span::empty(), + AlterTableOperation::ValidateConstraint { name } => name.span, } } } diff --git a/src/keywords.rs b/src/keywords.rs index a8bbca3d..49a54e8a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -981,6 +981,7 @@ define_keywords!( UUID, VACUUM, VALID, + VALIDATE, VALIDATION_MODE, VALUE, VALUES, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6360817f..1c40a964 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8477,7 +8477,11 @@ impl<'a> Parser<'a> { pub fn parse_alter_table_operation(&mut self) -> Result { let operation = if self.parse_keyword(Keyword::ADD) { if let Some(constraint) = self.parse_optional_table_constraint()? { - AlterTableOperation::AddConstraint(constraint) + let not_valid = self.parse_keywords(&[Keyword::NOT, Keyword::VALID]); + AlterTableOperation::AddConstraint { + constraint, + not_valid, + } } else if dialect_of!(self is ClickHouseDialect|GenericDialect) && self.parse_keyword(Keyword::PROJECTION) { @@ -8886,6 +8890,9 @@ impl<'a> Parser<'a> { }; AlterTableOperation::ReplicaIdentity { identity } + } else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) { + let name = self.parse_identifier()?; + AlterTableOperation::ValidateConstraint { name } } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; diff --git a/src/test_utils.rs b/src/test_utils.rs index db7b3dd6..544ceaef 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -479,20 +479,25 @@ pub fn index_column(stmt: Statement) -> Expr { } } Statement::AlterTable { operations, .. } => match operations.first().unwrap() { - AlterTableOperation::AddConstraint(TableConstraint::Index { columns, .. }) => { - columns.first().unwrap().column.expr.clone() + AlterTableOperation::AddConstraint { constraint, .. } => { + match constraint { + TableConstraint::Index { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::Unique { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::PrimaryKey { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial { + columns, + .. + } => columns.first().unwrap().column.expr.clone(), + _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + } } - AlterTableOperation::AddConstraint(TableConstraint::Unique { columns, .. }) => { - columns.first().unwrap().column.expr.clone() - } - AlterTableOperation::AddConstraint(TableConstraint::PrimaryKey { columns, .. }) => { - columns.first().unwrap().column.expr.clone() - } - AlterTableOperation::AddConstraint(TableConstraint::FulltextOrSpatial { - columns, - .. - }) => columns.first().unwrap().column.expr.clone(), - _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + _ => panic!("Expected a constraint"), }, _ => panic!("Expected CREATE INDEX, ALTER TABLE, or CREATE TABLE, got: {stmt:?}"), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 380bd475..9ca985b3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4956,7 +4956,7 @@ fn parse_alter_table_constraints() { match alter_table_op(verified_stmt(&format!( "ALTER TABLE tab ADD {constraint_text}" ))) { - AlterTableOperation::AddConstraint(constraint) => { + AlterTableOperation::AddConstraint { constraint, .. } => { assert_eq!(constraint_text, constraint.to_string()); } _ => unreachable!(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e1a49c69..16ba5f23 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -606,9 +606,10 @@ fn parse_alter_table_constraints_unique_nulls_distinct() { .verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)") { Statement::AlterTable { operations, .. } => match &operations[0] { - AlterTableOperation::AddConstraint(TableConstraint::Unique { - nulls_distinct, .. - }) => { + AlterTableOperation::AddConstraint { + constraint: TableConstraint::Unique { nulls_distinct, .. }, + .. + } => { assert_eq!(nulls_distinct, &NullsDistinctOption::NotDistinct) } _ => unreachable!(), @@ -6229,3 +6230,43 @@ fn parse_ts_datatypes() { _ => unreachable!(), } } + +#[test] +fn parse_alter_table_constraint_not_valid() { + match pg_and_generic().verified_stmt( + "ALTER TABLE foo ADD CONSTRAINT bar FOREIGN KEY (baz) REFERENCES other(ref) NOT VALID", + ) { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::AddConstraint { + constraint: TableConstraint::ForeignKey { + name: Some("bar".into()), + index_name: None, + columns: vec!["baz".into()], + foreign_table: ObjectName::from(vec!["other".into()]), + referred_columns: vec!["ref".into()], + on_delete: None, + on_update: None, + characteristics: None, + }, + not_valid: true, + }] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_validate_constraint() { + match pg_and_generic().verified_stmt("ALTER TABLE foo VALIDATE CONSTRAINT bar") { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::ValidateConstraint { name: "bar".into() }] + ); + } + _ => unreachable!(), + } +} From be2d2f14e740ac753d51609b026dae012b697353 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:22:17 +0200 Subject: [PATCH 114/130] Add support for MySQL MEMBER OF (#1917) --- src/ast/mod.rs | 24 ++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/mod.rs | 2 ++ src/parser/mod.rs | 13 +++++++++++++ tests/sqlparser_mysql.rs | 25 +++++++++++++++++++++++++ 5 files changed, 65 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 19966d21..9e502260 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1124,6 +1124,8 @@ pub enum Expr { /// [Databricks](https://docs.databricks.com/en/sql/language-manual/sql-ref-lambda-functions.html) /// [DuckDb](https://duckdb.org/docs/sql/functions/lambda.html) Lambda(LambdaFunction), + /// Checks membership of a value in a JSON array + MemberOf(MemberOf), } impl Expr { @@ -1912,6 +1914,7 @@ impl fmt::Display for Expr { } Expr::Prior(expr) => write!(f, "PRIOR {expr}"), Expr::Lambda(lambda) => write!(f, "{lambda}"), + Expr::MemberOf(member_of) => write!(f, "{member_of}"), } } } @@ -9831,6 +9834,27 @@ impl fmt::Display for NullInclusion { } } +/// Checks membership of a value in a JSON array +/// +/// Syntax: +/// ```sql +/// MEMBER OF() +/// ``` +/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/json-search-functions.html#operator_member-of) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MemberOf { + pub value: Box, + pub array: Box, +} + +impl fmt::Display for MemberOf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} MEMBER OF({})", self.value, self.array) + } +} + #[cfg(test)] mod tests { use crate::tokenizer::Location; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 26205496..f8ffeb54 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1624,6 +1624,7 @@ impl Spanned for Expr { Expr::OuterJoin(expr) => expr.span(), Expr::Prior(expr) => expr.span(), Expr::Lambda(_) => Span::empty(), + Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b214699c..3345380c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -649,6 +649,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)), _ => Ok(self.prec_unknown()), }, Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)), @@ -661,6 +662,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)), Token::Period => Ok(p!(Period)), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1c40a964..bfd75385 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3609,6 +3609,19 @@ impl<'a> Parser<'a> { self.expected("IN or BETWEEN after NOT", self.peek_token()) } } + Keyword::MEMBER => { + if self.parse_keyword(Keyword::OF) { + self.expect_token(&Token::LParen)?; + let array = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Ok(Expr::MemberOf(MemberOf { + value: Box::new(expr), + array: Box::new(array), + })) + } else { + self.expected("OF after MEMBER", self.peek_token()) + } + } // Can only happen if `get_next_precedence` got out of sync with this function _ => parser_err!( format!("No infix parser for token {:?}", tok.token), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index d2feee03..9224a003 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4109,3 +4109,28 @@ fn parse_alter_table_drop_index() { AlterTableOperation::DropIndex { name } if name.value == "idx_index" ); } + +#[test] +fn parse_json_member_of() { + mysql().verified_stmt(r#"SELECT 17 MEMBER OF('[23, "abc", 17, "ab", 10]')"#); + let sql = r#"SELECT 'ab' MEMBER OF('[23, "abc", 17, "ab", 10]')"#; + let stmt = mysql().verified_stmt(sql); + match stmt { + Statement::Query(query) => { + let select = query.body.as_select().unwrap(); + assert_eq!( + select.projection, + vec![SelectItem::UnnamedExpr(Expr::MemberOf(MemberOf { + value: Box::new(Expr::Value( + Value::SingleQuotedString("ab".to_string()).into() + )), + array: Box::new(Expr::Value( + Value::SingleQuotedString(r#"[23, "abc", 17, "ab", 10]"#.to_string()) + .into() + )), + }))] + ); + } + _ => panic!("Unexpected statement {stmt}"), + } +} From 9020385c027239979adbef5506c7be4a07cc594c Mon Sep 17 00:00:00 2001 From: feral-dot-io <70718208+feral-dot-io@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:24:51 +0000 Subject: [PATCH 115/130] Add span for `Expr::TypedString` (#1919) --- src/ast/mod.rs | 2 +- src/ast/spans.rs | 1 - src/parser/mod.rs | 5 +- tests/sqlparser_bigquery.rs | 77 ++++++++++++++++++------ tests/sqlparser_common.rs | 106 ++++++++++++++++++++++++++-------- tests/sqlparser_databricks.rs | 6 +- tests/sqlparser_postgres.rs | 5 +- 7 files changed, 155 insertions(+), 47 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9e502260..d7e342bd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -996,7 +996,7 @@ pub enum Expr { data_type: DataType, /// The value of the constant. /// Hint: you can unwrap the string value using `value.into_string()`. - value: Value, + value: ValueWithSpan, }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f8ffeb54..0895b844 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1415,7 +1415,6 @@ impl Spanned for AssignmentTarget { /// f.e. `IS NULL ` reports as `::span`. /// /// Missing spans: -/// - [Expr::TypedString] # missing span for data_type /// - [Expr::MatchAgainst] # MySQL specific /// - [Expr::RLike] # MySQL specific /// - [Expr::Struct] # BigQuery specific diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bfd75385..f38a360e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1521,7 +1521,7 @@ impl<'a> Parser<'a> { DataType::Custom(..) => parser_err!("dummy", loc), data_type => Ok(Expr::TypedString { data_type, - value: parser.parse_value()?.value, + value: parser.parse_value()?, }), } })?; @@ -1708,10 +1708,9 @@ impl<'a> Parser<'a> { } fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result { - let value: Value = self.parse_value()?.value; Ok(Expr::TypedString { data_type: DataType::GeometricType(kind), - value, + value: self.parse_value()?, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 2bcdb4e5..2ba54d3e 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -906,7 +906,10 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + }, }], fields: vec![StructField { field_name: None, @@ -965,9 +968,12 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() - ) + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() + ), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -998,7 +1004,12 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString( + "2008-12-25 15:30:00 America/Los_Angeles".into() + ), + span: Span::empty(), + }, }], fields: vec![StructField { field_name: None, @@ -1013,7 +1024,10 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("15:30:00".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("15:30:00".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1031,7 +1045,10 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1045,7 +1062,10 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1219,7 +1239,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1278,9 +1301,12 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() - ) + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() + ), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1311,7 +1337,12 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString( + "2008-12-25 15:30:00 America/Los_Angeles".into() + ), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1326,7 +1357,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("15:30:00".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("15:30:00".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1344,7 +1378,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1358,7 +1395,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -2393,7 +2433,10 @@ fn test_triple_quote_typed_strings() { assert_eq!( Expr::TypedString { data_type: DataType::JSON, - value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()) + value: ValueWithSpan { + value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()), + span: Span::empty(), + } }, expr ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9ca985b3..ac461bb2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5851,7 +5851,10 @@ fn parse_literal_date() { assert_eq!( &Expr::TypedString { data_type: DataType::Date, - value: Value::SingleQuotedString("1999-01-01".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01".into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -5864,7 +5867,10 @@ fn parse_literal_time() { assert_eq!( &Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("01:23:34".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("01:23:34".into()), + span: Span::empty(), + }, }, expr_from_projection(only(&select.projection)), ); @@ -5877,7 +5883,10 @@ fn parse_literal_datetime() { assert_eq!( &Expr::TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + }, }, expr_from_projection(only(&select.projection)), ); @@ -5890,7 +5899,10 @@ fn parse_literal_timestamp_without_time_zone() { assert_eq!( &Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("1999-01-01 01:23:34".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34".into()), + span: Span::empty(), + }, }, expr_from_projection(only(&select.projection)), ); @@ -5905,7 +5917,10 @@ fn parse_literal_timestamp_with_time_zone() { assert_eq!( &Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::Tz), - value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()), + span: Span::empty(), + }, }, expr_from_projection(only(&select.projection)), ); @@ -6477,8 +6492,9 @@ fn parse_json_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{ + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{ "id": 10, "type": "fruit", "name": "apple", @@ -6498,8 +6514,10 @@ fn parse_json_keyword() { ] } }"# - .to_string() - ) + .to_string() + ), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6511,7 +6529,10 @@ fn parse_typed_strings() { assert_eq!( Expr::TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()), + span: Span::empty(), + } }, expr ); @@ -6529,7 +6550,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"0"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"0"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6540,7 +6564,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"123456"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"123456"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6551,7 +6578,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-3.14"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-3.14"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6562,7 +6592,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-0.54321"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-0.54321"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6573,7 +6606,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"1.23456e05"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"1.23456e05"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6584,7 +6620,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-9.876e-3"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-9.876e-3"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -14833,7 +14872,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Point), - value: Value::SingleQuotedString("1,2".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2".to_string()), + span: Span::empty(), + }, } ); @@ -14842,7 +14884,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Line), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); @@ -14851,7 +14896,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::GeometricPath), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); let sql = "box '1,2,3,4'"; @@ -14859,7 +14907,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::GeometricBox), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); @@ -14868,7 +14919,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Circle), - value: Value::SingleQuotedString("1,2,3".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3".to_string()), + span: Span::empty(), + }, } ); @@ -14877,7 +14931,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Polygon), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); let sql = "lseg '1,2,3,4'"; @@ -14885,7 +14942,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::LineSegment), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index baf279fa..a27e0699 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -19,6 +19,7 @@ use sqlparser::ast::helpers::attached_token::AttachedToken; use sqlparser::ast::*; use sqlparser::dialect::{DatabricksDialect, GenericDialect}; use sqlparser::parser::ParserError; +use sqlparser::tokenizer::Span; use test_utils::*; #[macro_use] @@ -328,7 +329,10 @@ fn data_type_timestamp_ntz() { databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), Expr::TypedString { data_type: DataType::TimestampNtz, - value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()) + value: ValueWithSpan { + value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()), + span: Span::empty(), + } } ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 16ba5f23..db5c1611 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5257,7 +5257,10 @@ fn parse_at_time_zone() { left: Box::new(Expr::AtTimeZone { timestamp: Box::new(Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2001-09-28 01:00".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("2001-09-28 01:00".to_string()), + span: Span::empty(), + }, }), time_zone: Box::new(Expr::Cast { kind: CastKind::DoubleColon, From 239e30a97c5f088d39f1747fb108c8d8783a2e5c Mon Sep 17 00:00:00 2001 From: Sergey Olontsev Date: Thu, 3 Jul 2025 18:04:32 +0100 Subject: [PATCH 116/130] Support for Postgres `CREATE SERVER` (#1914) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 69 ++++++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 2 + src/parser/mod.rs | 45 +++++++++++++++++++++ tests/sqlparser_postgres.rs | 79 +++++++++++++++++++++++++++++++++++++ 5 files changed, 196 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d7e342bd..cffd3292 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3318,6 +3318,8 @@ pub enum Statement { secret_type: Ident, options: Vec, }, + /// A `CREATE SERVER` statement. + CreateServer(CreateServerStatement), /// ```sql /// CREATE POLICY /// ``` @@ -5178,6 +5180,9 @@ impl fmt::Display for Statement { write!(f, " )")?; Ok(()) } + Statement::CreateServer(stmt) => { + write!(f, "{stmt}") + } Statement::CreatePolicy { name, table_name, @@ -7976,6 +7981,70 @@ impl fmt::Display for SecretOption { } } +/// A `CREATE SERVER` statement. +/// +/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createserver.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateServerStatement { + pub name: ObjectName, + pub if_not_exists: bool, + pub server_type: Option, + pub version: Option, + pub foreign_data_wrapper: ObjectName, + pub options: Option>, +} + +impl fmt::Display for CreateServerStatement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let CreateServerStatement { + name, + if_not_exists, + server_type, + version, + foreign_data_wrapper, + options, + } = self; + + write!( + f, + "CREATE SERVER {if_not_exists}{name} ", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + + if let Some(st) = server_type { + write!(f, "TYPE {st} ")?; + } + + if let Some(v) = version { + write!(f, "VERSION {v} ")?; + } + + write!(f, "FOREIGN DATA WRAPPER {foreign_data_wrapper}")?; + + if let Some(o) = options { + write!(f, " OPTIONS ({o})", o = display_comma_separated(o))?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateServerOption { + pub key: Ident, + pub value: Ident, +} + +impl fmt::Display for CreateServerOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.key, self.value) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0895b844..1d790ab6 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -423,6 +423,7 @@ impl Spanned for Statement { Statement::CreateIndex(create_index) => create_index.span(), Statement::CreateRole { .. } => Span::empty(), Statement::CreateSecret { .. } => Span::empty(), + Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), Statement::AlterTable { name, diff --git a/src/keywords.rs b/src/keywords.rs index 49a54e8a..73865150 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -816,6 +816,7 @@ define_keywords!( SERDE, SERDEPROPERTIES, SERIALIZABLE, + SERVER, SERVICE, SESSION, SESSION_USER, @@ -1017,6 +1018,7 @@ define_keywords!( WITHOUT, WITHOUT_ARRAY_WRAPPER, WORK, + WRAPPER, WRITE, XML, XMLNAMESPACES, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f38a360e..289dd6b3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4674,6 +4674,8 @@ impl<'a> Parser<'a> { self.parse_create_procedure(or_alter) } else if self.parse_keyword(Keyword::CONNECTOR) { self.parse_create_connector() + } else if self.parse_keyword(Keyword::SERVER) { + self.parse_pg_create_server() } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -16009,6 +16011,49 @@ impl<'a> Parser<'a> { Ok(sequence_options) } + /// Parse a `CREATE SERVER` statement. + /// + /// See [Statement::CreateServer] + pub fn parse_pg_create_server(&mut self) -> Result { + let ine = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + + let server_type = if self.parse_keyword(Keyword::TYPE) { + Some(self.parse_identifier()?) + } else { + None + }; + + let version = if self.parse_keyword(Keyword::VERSION) { + Some(self.parse_identifier()?) + } else { + None + }; + + self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?; + let foreign_data_wrapper = self.parse_object_name(false)?; + + let mut options = None; + if self.parse_keyword(Keyword::OPTIONS) { + self.expect_token(&Token::LParen)?; + options = Some(self.parse_comma_separated(|p| { + let key = p.parse_identifier()?; + let value = p.parse_identifier()?; + Ok(CreateServerOption { key, value }) + })?); + self.expect_token(&Token::RParen)?; + } + + Ok(Statement::CreateServer(CreateServerStatement { + name, + if_not_exists: ine, + server_type, + version, + foreign_data_wrapper, + options, + })) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index db5c1611..48792025 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6273,3 +6273,82 @@ fn parse_alter_table_validate_constraint() { _ => unreachable!(), } } + +#[test] +fn parse_create_server() { + let test_cases = vec![ + ( + "CREATE SERVER myserver FOREIGN DATA WRAPPER postgres_fdw", + CreateServerStatement { + name: ObjectName::from(vec!["myserver".into()]), + if_not_exists: false, + server_type: None, + version: None, + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: None, + }, + ), + ( + "CREATE SERVER IF NOT EXISTS myserver TYPE 'server_type' VERSION 'server_version' FOREIGN DATA WRAPPER postgres_fdw", + CreateServerStatement { + name: ObjectName::from(vec!["myserver".into()]), + if_not_exists: true, + server_type: Some(Ident { + value: "server_type".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + version: Some(Ident { + value: "server_version".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: None, + } + ), + ( + "CREATE SERVER myserver2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foo', dbname 'foodb', port '5432')", + CreateServerStatement { + name: ObjectName::from(vec!["myserver2".into()]), + if_not_exists: false, + server_type: None, + version: None, + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: Some(vec![ + CreateServerOption { + key: "host".into(), + value: Ident { + value: "foo".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + CreateServerOption { + key: "dbname".into(), + value: Ident { + value: "foodb".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + CreateServerOption { + key: "port".into(), + value: Ident { + value: "5432".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + ]), + } + ) + ]; + + for (sql, expected) in test_cases { + let Statement::CreateServer(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt, expected); + } +} From 942d747d893ad1c25fc32b002066b3bda72071b6 Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Fri, 4 Jul 2025 18:21:31 +0200 Subject: [PATCH 117/130] Change tag and policy names to `ObjectName` (#1892) --- src/ast/ddl.rs | 2 +- src/ast/mod.rs | 4 +-- src/dialect/snowflake.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_snowflake.rs | 64 +++++++++++++++++++++++++++--------- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9d500203..7e46a59f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1709,7 +1709,7 @@ pub struct ColumnPolicyProperty { /// ``` /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table pub with: bool, - pub policy_name: Ident, + pub policy_name: ObjectName, pub using_columns: Option>, } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cffd3292..7db4af11 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9372,12 +9372,12 @@ impl Display for RowAccessPolicy { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Tag { - pub key: Ident, + pub key: ObjectName, pub value: String, } impl Tag { - pub fn new(key: Ident, value: String) -> Self { + pub fn new(key: ObjectName, value: String) -> Self { Self { key, value } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ba28a8ec..ee770b0f 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1189,7 +1189,7 @@ fn parse_column_policy_property( parser: &mut Parser, with: bool, ) -> Result { - let policy_name = parser.parse_identifier()?; + let policy_name = parser.parse_object_name(false)?; let using_columns = if parser.parse_keyword(Keyword::USING) { parser.expect_token(&Token::LParen)?; let columns = parser.parse_comma_separated(|p| p.parse_identifier())?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 289dd6b3..612144a6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7884,7 +7884,7 @@ impl<'a> Parser<'a> { } pub(crate) fn parse_tag(&mut self) -> Result { - let name = self.parse_identifier()?; + let name = self.parse_object_name(false)?; self.expect_token(&Token::Eq)?; let value = self.parse_literal_string()?; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 3ffd3339..8a1558b2 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -270,8 +270,8 @@ fn test_snowflake_create_table_with_tag() { assert_eq!("my_table", name.to_string()); assert_eq!( Some(vec![ - Tag::new("A".into(), "TAG A".to_string()), - Tag::new("B".into(), "TAG B".to_string()) + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".to_string()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".to_string()) ]), with_tags ); @@ -291,8 +291,8 @@ fn test_snowflake_create_table_with_tag() { assert_eq!("my_table", name.to_string()); assert_eq!( Some(vec![ - Tag::new("A".into(), "TAG A".to_string()), - Tag::new("B".into(), "TAG B".to_string()) + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".to_string()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".to_string()) ]), with_tags ); @@ -731,7 +731,7 @@ fn test_snowflake_create_table_with_columns_masking_policy() { option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( ColumnPolicyProperty { with, - policy_name: "p".into(), + policy_name: ObjectName::from(vec![Ident::new("p")]), using_columns, } )) @@ -765,7 +765,7 @@ fn test_snowflake_create_table_with_columns_projection_policy() { option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( ColumnPolicyProperty { with, - policy_name: "p".into(), + policy_name: ObjectName::from(vec![Ident::new("p")]), using_columns: None, } )) @@ -802,8 +802,14 @@ fn test_snowflake_create_table_with_columns_tags() { option: ColumnOption::Tags(TagsColumnOption { with, tags: vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new( + ObjectName::from(vec![Ident::new("A")]), + "TAG A".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("B")]), + "TAG B".into() + ), ] }), }], @@ -846,7 +852,7 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( ColumnPolicyProperty { with: true, - policy_name: "p1".into(), + policy_name: ObjectName::from(vec![Ident::new("p1")]), using_columns: Some(vec!["a".into(), "b".into()]), } )), @@ -856,8 +862,14 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Tags(TagsColumnOption { with: true, tags: vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new( + ObjectName::from(vec![Ident::new("A")]), + "TAG A".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("B")]), + "TAG B".into() + ), ] }), } @@ -878,7 +890,7 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( ColumnPolicyProperty { with: false, - policy_name: "p2".into(), + policy_name: ObjectName::from(vec![Ident::new("p2")]), using_columns: None, } )), @@ -888,8 +900,14 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Tags(TagsColumnOption { with: false, tags: vec![ - Tag::new("C".into(), "TAG C".into()), - Tag::new("D".into(), "TAG D".into()), + Tag::new( + ObjectName::from(vec![Ident::new("C")]), + "TAG C".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("D")]), + "TAG D".into() + ), ] }), } @@ -942,8 +960,8 @@ fn test_snowflake_create_iceberg_table_all_options() { with_aggregation_policy.map(|name| name.to_string()) ); assert_eq!(Some(vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".into()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".into()), ]), with_tags); } @@ -4172,3 +4190,17 @@ fn test_snowflake_create_view_with_multiple_column_options() { r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; snowflake().verified_stmt(create_view_with_tag); } + +#[test] +fn test_snowflake_create_view_with_composite_tag() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH TAG (foo.bar.baz.pii='email')) AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} + +#[test] +fn test_snowflake_create_view_with_composite_policy_name() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH MASKING POLICY foo.bar.baz) AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} From b0bcc46e227162b4a9147c9c1e0a88bf3e0b5e1f Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 4 Jul 2025 21:04:51 +0200 Subject: [PATCH 118/130] Add support for NULL escape char in pattern match searches (#1913) --- src/ast/mod.rs | 12 ++++++------ src/parser/mod.rs | 4 ++-- tests/sqlparser_common.rs | 25 +++++++++++++++++++++---- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7db4af11..425e1fb6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -809,7 +809,7 @@ pub enum Expr { any: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// `ILIKE` (case-insensitive `LIKE`) ILike { @@ -819,14 +819,14 @@ pub enum Expr { any: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// SIMILAR TO regex SimilarTo { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// MySQL: RLIKE regex or REGEXP regex RLike { @@ -1488,7 +1488,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}LIKE {}{} ESCAPE '{}'", + "{} {}LIKE {}{} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, if *any { "ANY " } else { "" }, @@ -1513,7 +1513,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}ILIKE {}{} ESCAPE '{}'", + "{} {}ILIKE {}{} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, if *any { "ANY" } else { "" }, @@ -1568,7 +1568,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}SIMILAR TO {} ESCAPE '{}'", + "{} {}SIMILAR TO {} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, pattern, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 612144a6..32f8a97e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3654,9 +3654,9 @@ impl<'a> Parser<'a> { } /// Parse the `ESCAPE CHAR` portion of `LIKE`, `ILIKE`, and `SIMILAR TO` - pub fn parse_escape_char(&mut self) -> Result, ParserError> { + pub fn parse_escape_char(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::ESCAPE) { - Ok(Some(self.parse_literal_string()?)) + Ok(Some(self.parse_value()?.into())) } else { Ok(None) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ac461bb2..1bdd302e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2040,7 +2040,7 @@ fn parse_ilike() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), any: false, }, select.selection.unwrap() @@ -2104,7 +2104,7 @@ fn parse_like() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), any: false, }, select.selection.unwrap() @@ -2167,7 +2167,24 @@ fn parse_similar_to() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), + }, + select.selection.unwrap() + ); + + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE NULL", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), + escape_char: Some(Value::Null), }, select.selection.unwrap() ); @@ -2185,7 +2202,7 @@ fn parse_similar_to() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), })), select.selection.unwrap() ); From d2466af20a5336c1554bdb82b0c84d04a20db26e Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:18:58 +0200 Subject: [PATCH 119/130] Add support for dropping multiple columns in Snowflake (#1918) --- src/ast/ddl.rs | 8 ++++---- src/ast/spans.rs | 4 ++-- src/dialect/mod.rs | 5 +++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 8 ++++++-- tests/sqlparser_common.rs | 7 +++++-- tests/sqlparser_mysql.rs | 4 ++-- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 7e46a59f..51e05784 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -140,10 +140,10 @@ pub enum AlterTableOperation { name: Ident, drop_behavior: Option, }, - /// `DROP [ COLUMN ] [ IF EXISTS ] [ CASCADE ]` + /// `DROP [ COLUMN ] [ IF EXISTS ] [ , , ... ] [ CASCADE ]` DropColumn { has_column_keyword: bool, - column_name: Ident, + column_names: Vec, if_exists: bool, drop_behavior: Option, }, @@ -631,7 +631,7 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::DropIndex { name } => write!(f, "DROP INDEX {name}"), AlterTableOperation::DropColumn { has_column_keyword, - column_name, + column_names: column_name, if_exists, drop_behavior, } => write!( @@ -639,7 +639,7 @@ impl fmt::Display for AlterTableOperation { "DROP {}{}{}{}", if *has_column_keyword { "COLUMN " } else { "" }, if *if_exists { "IF EXISTS " } else { "" }, - column_name, + display_comma_separated(column_name), match drop_behavior { None => "", Some(DropBehavior::Restrict) => " RESTRICT", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1d790ab6..144de592 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1112,10 +1112,10 @@ impl Spanned for AlterTableOperation { } => name.span, AlterTableOperation::DropColumn { has_column_keyword: _, - column_name, + column_names, if_exists: _, drop_behavior: _, - } => column_name.span, + } => union_spans(column_names.iter().map(|i| i.span)), AlterTableOperation::AttachPartition { partition } => partition.span(), AlterTableOperation::DetachPartition { partition } => partition.span(), AlterTableOperation::FreezePartition { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3345380c..bc3c5555 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1071,6 +1071,11 @@ pub trait Dialect: Debug + Any { fn supports_alter_column_type_using(&self) -> bool { false } + + /// Returns true if the dialect supports `ALTER TABLE tbl DROP COLUMN c1, ..., cn` + fn supports_comma_separated_drop_column_list(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ee770b0f..f56db9b8 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -364,6 +364,10 @@ impl Dialect for SnowflakeDialect { fn supports_space_separated_column_options(&self) -> bool { true } + + fn supports_comma_separated_drop_column_list(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 32f8a97e..839d3645 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8675,11 +8675,15 @@ impl<'a> Parser<'a> { } else { let has_column_keyword = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let column_name = self.parse_identifier()?; + let column_names = if self.dialect.supports_comma_separated_drop_column_list() { + self.parse_comma_separated(Parser::parse_identifier)? + } else { + vec![self.parse_identifier()?] + }; let drop_behavior = self.parse_optional_drop_behavior(); AlterTableOperation::DropColumn { has_column_keyword, - column_name, + column_names, if_exists, drop_behavior, } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1bdd302e..ed9bb704 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4996,15 +4996,18 @@ fn parse_alter_table_drop_column() { "ALTER TABLE tab DROP is_active CASCADE", ); + let dialects = all_dialects_where(|d| d.supports_comma_separated_drop_column_list()); + dialects.verified_stmt("ALTER TABLE tbl DROP COLUMN c1, c2, c3"); + fn check_one(constraint_text: &str) { match alter_table_op(verified_stmt(&format!("ALTER TABLE tab {constraint_text}"))) { AlterTableOperation::DropColumn { has_column_keyword: true, - column_name, + column_names, if_exists, drop_behavior, } => { - assert_eq!("is_active", column_name.to_string()); + assert_eq!("is_active", column_names.first().unwrap().to_string()); assert!(if_exists); match drop_behavior { None => assert!(constraint_text.ends_with(" is_active")), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 9224a003..79e2259b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2876,7 +2876,7 @@ fn parse_alter_table_with_algorithm() { vec![ AlterTableOperation::DropColumn { has_column_keyword: true, - column_name: Ident::new("password_digest"), + column_names: vec![Ident::new("password_digest")], if_exists: false, drop_behavior: None, }, @@ -2924,7 +2924,7 @@ fn parse_alter_table_with_lock() { vec![ AlterTableOperation::DropColumn { has_column_keyword: true, - column_name: Ident::new("password_digest"), + column_names: vec![Ident::new("password_digest")], if_exists: false, drop_behavior: None, }, From ed8757f2f0596c3e27b7f878e012bfc4d6da32a2 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:40:35 +0200 Subject: [PATCH 120/130] Align Snowflake dialect to new test of reserved keywords (#1924) --- src/dialect/snowflake.rs | 5 ++--- tests/sqlparser_snowflake.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index f56db9b8..212cf217 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -301,9 +301,8 @@ impl Dialect for SnowflakeDialect { true } - fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { - explicit - || match kw { + fn is_column_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool { + match kw { // The following keywords can be considered an alias as long as // they are not followed by other tokens that may change their meaning // e.g. `SELECT * EXCEPT (col1) FROM tbl` diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 8a1558b2..e7393d3f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3415,10 +3415,38 @@ fn parse_ls_and_rm() { .unwrap(); } +#[test] +fn test_sql_keywords_as_select_item_ident() { + // Some keywords that should be parsed as an alias + let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT", "SORT"]; + for kw in unreserved_kws { + snowflake().verified_stmt(&format!("SELECT 1, {kw}")); + } + + // Some keywords that should not be parsed as an alias + let reserved_kws = vec![ + "FROM", + "GROUP", + "HAVING", + "INTERSECT", + "INTO", + "ORDER", + "SELECT", + "UNION", + "WHERE", + "WITH", + ]; + for kw in reserved_kws { + assert!(snowflake() + .parse_sql_statements(&format!("SELECT 1, {kw}")) + .is_err()); + } +} + #[test] fn test_sql_keywords_as_select_item_aliases() { // Some keywords that should be parsed as an alias - let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT"]; + let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT", "SORT"]; for kw in unreserved_kws { snowflake() .one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {kw}")); From cf9e50474ec6c59027bcc3e2f418c5ff6947920a Mon Sep 17 00:00:00 2001 From: Simon Vandel Sillesen Date: Sun, 6 Jul 2025 08:57:20 +0200 Subject: [PATCH 121/130] Make `GenericDialect` support trailing commas in projections (#1921) --- src/dialect/generic.rs | 4 ++++ tests/sqlparser_common.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 59671e21..5e9f2e4e 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -116,6 +116,10 @@ impl Dialect for GenericDialect { true } + fn supports_projection_trailing_commas(&self) -> bool { + true + } + fn supports_asc_desc_in_column_definition(&self) -> bool { true } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ed9bb704..56b015fc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11184,7 +11184,7 @@ fn parse_trailing_comma() { trailing_commas.verified_stmt(r#"SELECT "from" FROM "from""#); // doesn't allow any trailing commas - let trailing_commas = TestedDialects::new(vec![Box::new(GenericDialect {})]); + let trailing_commas = TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]); assert_eq!( trailing_commas From f2fba48a7a7c2cba43baafa3c87b9f7abe85c869 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 6 Jul 2025 08:58:19 +0200 Subject: [PATCH 122/130] Add support for several Snowflake grant statements (#1922) --- src/ast/mod.rs | 54 ++++++++++++++++++++++++++++++++++++ src/parser/mod.rs | 58 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 6 ++++ 3 files changed, 118 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 425e1fb6..93ef7663 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6928,12 +6928,24 @@ pub enum GrantObjects { AllSequencesInSchema { schemas: Vec }, /// Grant privileges on `ALL TABLES IN SCHEMA [, ...]` AllTablesInSchema { schemas: Vec }, + /// Grant privileges on `ALL VIEWS IN SCHEMA [, ...]` + AllViewsInSchema { schemas: Vec }, + /// Grant privileges on `ALL MATERIALIZED VIEWS IN SCHEMA [, ...]` + AllMaterializedViewsInSchema { schemas: Vec }, + /// Grant privileges on `ALL EXTERNAL TABLES IN SCHEMA [, ...]` + AllExternalTablesInSchema { schemas: Vec }, /// Grant privileges on `FUTURE SCHEMAS IN DATABASE [, ...]` FutureSchemasInDatabase { databases: Vec }, /// Grant privileges on `FUTURE TABLES IN SCHEMA [, ...]` FutureTablesInSchema { schemas: Vec }, /// Grant privileges on `FUTURE VIEWS IN SCHEMA [, ...]` FutureViewsInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE EXTERNAL TABLES IN SCHEMA [, ...]` + FutureExternalTablesInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE MATERIALIZED VIEWS IN SCHEMA [, ...]` + FutureMaterializedViewsInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE SEQUENCES IN SCHEMA [, ...]` + FutureSequencesInSchema { schemas: Vec }, /// Grant privileges on specific databases Databases(Vec), /// Grant privileges on specific schemas @@ -7002,6 +7014,27 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::AllExternalTablesInSchema { schemas } => { + write!( + f, + "ALL EXTERNAL TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllViewsInSchema { schemas } => { + write!( + f, + "ALL VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllMaterializedViewsInSchema { schemas } => { + write!( + f, + "ALL MATERIALIZED VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::FutureSchemasInDatabase { databases } => { write!( f, @@ -7016,6 +7049,13 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::FutureExternalTablesInSchema { schemas } => { + write!( + f, + "FUTURE EXTERNAL TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::FutureViewsInSchema { schemas } => { write!( f, @@ -7023,6 +7063,20 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::FutureMaterializedViewsInSchema { schemas } => { + write!( + f, + "FUTURE MATERIALIZED VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureSequencesInSchema { schemas } => { + write!( + f, + "FUTURE SEQUENCES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::ResourceMonitors(objects) => { write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects)) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 839d3645..a94c7b43 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13901,6 +13901,35 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllTablesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::EXTERNAL, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllExternalTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::MATERIALIZED, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllMaterializedViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::FUTURE, Keyword::SCHEMAS, @@ -13919,6 +13948,16 @@ impl<'a> Parser<'a> { Some(GrantObjects::FutureTablesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::EXTERNAL, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureExternalTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::FUTURE, Keyword::VIEWS, @@ -13928,6 +13967,16 @@ impl<'a> Parser<'a> { Some(GrantObjects::FutureViewsInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::MATERIALIZED, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureMaterializedViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::ALL, Keyword::SEQUENCES, @@ -13937,6 +13986,15 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllSequencesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::SEQUENCES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureSequencesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[Keyword::RESOURCE, Keyword::MONITOR]) { Some(GrantObjects::ResourceMonitors(self.parse_comma_separated( |p| p.parse_object_name_with_wildcards(false, true), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 56b015fc..0d3c4d82 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9434,6 +9434,9 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO APPLICATION role1"); verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO APPLICATION ROLE role1"); verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO SHARE share1"); + verified_stmt("GRANT SELECT ON ALL VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON ALL MATERIALIZED VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON ALL EXTERNAL TABLES IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT USAGE ON SCHEMA sc1 TO a:b"); verified_stmt("GRANT USAGE ON SCHEMA sc1 TO GROUP group1"); verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST"); @@ -9447,7 +9450,10 @@ fn parse_grant() { .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); verified_stmt("GRANT SELECT ON FUTURE SCHEMAS IN DATABASE db1 TO ROLE role1"); verified_stmt("GRANT SELECT ON FUTURE TABLES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE EXTERNAL TABLES IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT SELECT ON FUTURE VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE MATERIALIZED VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE SEQUENCES IN SCHEMA db1.sc1 TO ROLE role1"); } #[test] From 1a33abda63658f19902026df99826cce74812c02 Mon Sep 17 00:00:00 2001 From: Sergey Olontsev Date: Sun, 6 Jul 2025 08:06:20 +0100 Subject: [PATCH 123/130] Clickhouse: support empty parenthesized options (#1925) --- src/parser/mod.rs | 4 ++-- tests/sqlparser_clickhouse.rs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a94c7b43..61944b8f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16253,9 +16253,9 @@ impl<'a> Parser<'a> { fn parse_parenthesized_identifiers(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(|p| p.parse_identifier())?; + let idents = self.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; self.expect_token(&Token::RParen)?; - Ok(partitions) + Ok(idents) } fn parse_column_position(&mut self) -> Result, ParserError> { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 0288c6d2..1d8669a2 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -224,6 +224,10 @@ fn parse_create_table() { clickhouse().verified_stmt( r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY "x" AS SELECT * FROM "t" WHERE true"#, ); + clickhouse().one_statement_parses_to( + "CREATE TABLE x (a int) ENGINE = MergeTree() ORDER BY a", + "CREATE TABLE x (a INT) ENGINE = MergeTree ORDER BY a", + ); } #[test] From 93450cc2505388f546623c9807f1320acf4fe880 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:13:57 +0200 Subject: [PATCH 124/130] Add Snowflake `COPY/REVOKE CURRENT GRANTS` option (#1926) --- src/ast/mod.rs | 27 +++++++++++++++++++++++++++ src/parser/mod.rs | 10 ++++++++++ tests/sqlparser_common.rs | 2 ++ tests/sqlparser_mysql.rs | 1 + 4 files changed, 40 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 93ef7663..8da6bbe7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3996,6 +3996,7 @@ pub enum Statement { with_grant_option: bool, as_grantor: Option, granted_by: Option, + current_grants: Option, }, /// ```sql /// DENY privileges ON object TO grantees @@ -4312,6 +4313,28 @@ pub enum Statement { Return(ReturnStatement), } +/// ```sql +/// {COPY | REVOKE} CURRENT GRANTS +/// ``` +/// +/// - [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#optional-parameters) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CurrentGrantsKind { + CopyCurrentGrants, + RevokeCurrentGrants, +} + +impl fmt::Display for CurrentGrantsKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CurrentGrantsKind::CopyCurrentGrants => write!(f, "COPY CURRENT GRANTS"), + CurrentGrantsKind::RevokeCurrentGrants => write!(f, "REVOKE CURRENT GRANTS"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -5715,6 +5738,7 @@ impl fmt::Display for Statement { with_grant_option, as_grantor, granted_by, + current_grants, } => { write!(f, "GRANT {privileges} ")?; if let Some(objects) = objects { @@ -5724,6 +5748,9 @@ impl fmt::Display for Statement { if *with_grant_option { write!(f, " WITH GRANT OPTION")?; } + if let Some(current_grants) = current_grants { + write!(f, " {current_grants}")?; + } if let Some(grantor) = as_grantor { write!(f, " AS {grantor}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 61944b8f..c4d0508d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13794,6 +13794,15 @@ impl<'a> Parser<'a> { let with_grant_option = self.parse_keywords(&[Keyword::WITH, Keyword::GRANT, Keyword::OPTION]); + let current_grants = + if self.parse_keywords(&[Keyword::COPY, Keyword::CURRENT, Keyword::GRANTS]) { + Some(CurrentGrantsKind::CopyCurrentGrants) + } else if self.parse_keywords(&[Keyword::REVOKE, Keyword::CURRENT, Keyword::GRANTS]) { + Some(CurrentGrantsKind::RevokeCurrentGrants) + } else { + None + }; + let as_grantor = if self.parse_keywords(&[Keyword::AS]) { Some(self.parse_identifier()?) } else { @@ -13813,6 +13822,7 @@ impl<'a> Parser<'a> { with_grant_option, as_grantor, granted_by, + current_grants, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0d3c4d82..27fe09c7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9440,6 +9440,8 @@ fn parse_grant() { verified_stmt("GRANT USAGE ON SCHEMA sc1 TO a:b"); verified_stmt("GRANT USAGE ON SCHEMA sc1 TO GROUP group1"); verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST"); + verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST COPY CURRENT GRANTS"); + verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST REVOKE CURRENT GRANTS"); verified_stmt("GRANT USAGE ON DATABASE db1 TO ROLE role1"); verified_stmt("GRANT USAGE ON WAREHOUSE wh1 TO ROLE role1"); verified_stmt("GRANT OWNERSHIP ON INTEGRATION int1 TO ROLE role1"); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 79e2259b..44f36584 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3616,6 +3616,7 @@ fn parse_grant() { with_grant_option, as_grantor: _, granted_by, + current_grants: _, } = stmt { assert_eq!( From b1a6d11e123ff5432d24e4baf3028176649692f3 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:15:50 +0200 Subject: [PATCH 125/130] Add support for Snowflake identifier function (#1929) --- src/ast/mod.rs | 22 +++++ src/ast/spans.rs | 4 + src/dialect/mod.rs | 15 +++- src/dialect/snowflake.rs | 21 ++++- src/parser/mod.rs | 152 ++++++++++++++++++++--------------- tests/sqlparser_common.rs | 6 +- tests/sqlparser_snowflake.rs | 135 +++++++++++++++++++++++++++++++ 7 files changed, 286 insertions(+), 69 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8da6bbe7..619730e2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -344,12 +344,14 @@ impl fmt::Display for ObjectName { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ObjectNamePart { Identifier(Ident), + Function(ObjectNamePartFunction), } impl ObjectNamePart { pub fn as_ident(&self) -> Option<&Ident> { match self { ObjectNamePart::Identifier(ident) => Some(ident), + ObjectNamePart::Function(_) => None, } } } @@ -358,10 +360,30 @@ impl fmt::Display for ObjectNamePart { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ObjectNamePart::Identifier(ident) => write!(f, "{ident}"), + ObjectNamePart::Function(func) => write!(f, "{func}"), } } } +/// An object name part that consists of a function that dynamically +/// constructs identifiers. +/// +/// - [Snowflake](https://docs.snowflake.com/en/sql-reference/identifier-literal) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ObjectNamePartFunction { + pub name: Ident, + pub args: Vec, +} + +impl fmt::Display for ObjectNamePartFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}(", self.name)?; + write!(f, "{})", display_comma_separated(&self.args)) + } +} + /// Represents an Array Expression, either /// `ARRAY[..]`, or `[..]` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 144de592..a1b2e4e0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1671,6 +1671,10 @@ impl Spanned for ObjectNamePart { fn span(&self) -> Span { match self { ObjectNamePart::Identifier(ident) => ident.span, + ObjectNamePart::Function(func) => func + .name + .span + .union(&union_spans(func.args.iter().map(|i| i.span()))), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index bc3c5555..8f9dd617 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -49,7 +49,7 @@ pub use self::postgresql::PostgreSqlDialect; pub use self::redshift::RedshiftSqlDialect; pub use self::snowflake::SnowflakeDialect; pub use self::sqlite::SQLiteDialect; -use crate::ast::{ColumnOption, Expr, GranteesType, Statement}; +use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement}; pub use crate::keywords; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -1076,6 +1076,19 @@ pub trait Dialect: Debug + Any { fn supports_comma_separated_drop_column_list(&self) -> bool { false } + + /// Returns true if the dialect considers the specified ident as a function + /// that returns an identifier. Typically used to generate identifiers + /// programmatically. + /// + /// - [Snowflake](https://docs.snowflake.com/en/sql-reference/identifier-literal) + fn is_identifier_generating_function_name( + &self, + _ident: &Ident, + _name_parts: &[ObjectNamePart], + ) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 212cf217..e005300e 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -25,8 +25,8 @@ use crate::ast::helpers::stmt_data_loading::{ use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, SqlOption, Statement, - TagsColumnOption, WrappedCollection, + IdentityPropertyOrder, ObjectName, ObjectNamePart, RowAccessPolicy, ShowObjects, SqlOption, + Statement, TagsColumnOption, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -367,6 +367,23 @@ impl Dialect for SnowflakeDialect { fn supports_comma_separated_drop_column_list(&self) -> bool { true } + + fn is_identifier_generating_function_name( + &self, + ident: &Ident, + name_parts: &[ObjectNamePart], + ) -> bool { + ident.quote_style.is_none() + && ident.value.to_lowercase() == "identifier" + && !name_parts + .iter() + .any(|p| matches!(p, ObjectNamePart::Function(_))) + } + + // For example: `SELECT IDENTIFIER('alias1').* FROM tbl AS alias1` + fn supports_select_expr_star(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c4d0508d..5a42bb9e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10353,49 +10353,14 @@ impl<'a> Parser<'a> { } } - /// Parse a possibly qualified, possibly quoted identifier, optionally allowing for wildcards, - /// e.g. *, *.*, `foo`.*, or "foo"."bar" - fn parse_object_name_with_wildcards( - &mut self, - in_table_clause: bool, - allow_wildcards: bool, - ) -> Result { - let mut idents = vec![]; - - if dialect_of!(self is BigQueryDialect) && in_table_clause { - loop { - let (ident, end_with_period) = self.parse_unquoted_hyphenated_identifier()?; - idents.push(ident); - if !self.consume_token(&Token::Period) && !end_with_period { - break; - } - } - } else { - loop { - let ident = if allow_wildcards && self.peek_token().token == Token::Mul { - let span = self.next_token().span; - Ident { - value: Token::Mul.to_string(), - quote_style: None, - span, - } - } else { - if self.dialect.supports_object_name_double_dot_notation() - && idents.len() == 1 - && self.consume_token(&Token::Period) - { - // Empty string here means default schema - idents.push(Ident::new("")); - } - self.parse_identifier()? - }; - idents.push(ident); - if !self.consume_token(&Token::Period) { - break; - } - } - } - Ok(ObjectName::from(idents)) + /// Parse a possibly qualified, possibly quoted identifier, e.g. + /// `foo` or `myschema."table" + /// + /// The `in_table_clause` parameter indicates whether the object name is a table in a FROM, JOIN, + /// or similar table clause. Currently, this is used only to support unquoted hyphenated identifiers + /// in this context on BigQuery. + pub fn parse_object_name(&mut self, in_table_clause: bool) -> Result { + self.parse_object_name_inner(in_table_clause, false) } /// Parse a possibly qualified, possibly quoted identifier, e.g. @@ -10404,19 +10369,76 @@ impl<'a> Parser<'a> { /// The `in_table_clause` parameter indicates whether the object name is a table in a FROM, JOIN, /// or similar table clause. Currently, this is used only to support unquoted hyphenated identifiers /// in this context on BigQuery. - pub fn parse_object_name(&mut self, in_table_clause: bool) -> Result { - let ObjectName(mut idents) = - self.parse_object_name_with_wildcards(in_table_clause, false)?; + /// + /// The `allow_wildcards` parameter indicates whether to allow for wildcards in the object name + /// e.g. *, *.*, `foo`.*, or "foo"."bar" + fn parse_object_name_inner( + &mut self, + in_table_clause: bool, + allow_wildcards: bool, + ) -> Result { + let mut parts = vec![]; + if dialect_of!(self is BigQueryDialect) && in_table_clause { + loop { + let (ident, end_with_period) = self.parse_unquoted_hyphenated_identifier()?; + parts.push(ObjectNamePart::Identifier(ident)); + if !self.consume_token(&Token::Period) && !end_with_period { + break; + } + } + } else { + loop { + if allow_wildcards && self.peek_token().token == Token::Mul { + let span = self.next_token().span; + parts.push(ObjectNamePart::Identifier(Ident { + value: Token::Mul.to_string(), + quote_style: None, + span, + })); + } else if dialect_of!(self is BigQueryDialect) && in_table_clause { + let (ident, end_with_period) = self.parse_unquoted_hyphenated_identifier()?; + parts.push(ObjectNamePart::Identifier(ident)); + if !self.consume_token(&Token::Period) && !end_with_period { + break; + } + } else if self.dialect.supports_object_name_double_dot_notation() + && parts.len() == 1 + && matches!(self.peek_token().token, Token::Period) + { + // Empty string here means default schema + parts.push(ObjectNamePart::Identifier(Ident::new(""))); + } else { + let ident = self.parse_identifier()?; + let part = if self + .dialect + .is_identifier_generating_function_name(&ident, &parts) + { + self.expect_token(&Token::LParen)?; + let args: Vec = + self.parse_comma_separated0(Self::parse_function_args, Token::RParen)?; + self.expect_token(&Token::RParen)?; + ObjectNamePart::Function(ObjectNamePartFunction { name: ident, args }) + } else { + ObjectNamePart::Identifier(ident) + }; + parts.push(part); + } + + if !self.consume_token(&Token::Period) { + break; + } + } + } // BigQuery accepts any number of quoted identifiers of a table name. // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#quoted_identifiers if dialect_of!(self is BigQueryDialect) - && idents.iter().any(|part| { + && parts.iter().any(|part| { part.as_ident() .is_some_and(|ident| ident.value.contains('.')) }) { - idents = idents + parts = parts .into_iter() .flat_map(|part| match part.as_ident() { Some(ident) => ident @@ -10435,7 +10457,7 @@ impl<'a> Parser<'a> { .collect() } - Ok(ObjectName(idents)) + Ok(ObjectName(parts)) } /// Parse identifiers @@ -14006,25 +14028,25 @@ impl<'a> Parser<'a> { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) } else if self.parse_keywords(&[Keyword::RESOURCE, Keyword::MONITOR]) { - Some(GrantObjects::ResourceMonitors(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::ResourceMonitors( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else if self.parse_keywords(&[Keyword::COMPUTE, Keyword::POOL]) { - Some(GrantObjects::ComputePools(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::ComputePools( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else if self.parse_keywords(&[Keyword::FAILOVER, Keyword::GROUP]) { - Some(GrantObjects::FailoverGroup(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::FailoverGroup( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else if self.parse_keywords(&[Keyword::REPLICATION, Keyword::GROUP]) { - Some(GrantObjects::ReplicationGroup(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::ReplicationGroup( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else if self.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) { - Some(GrantObjects::ExternalVolumes(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::ExternalVolumes( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else { let object_type = self.parse_one_of_keywords(&[ Keyword::SEQUENCE, @@ -14041,7 +14063,7 @@ impl<'a> Parser<'a> { Keyword::CONNECTION, ]); let objects = - self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); + self.parse_comma_separated(|p| p.parse_object_name_inner(false, true)); match object_type { Some(Keyword::DATABASE) => Some(GrantObjects::Databases(objects?)), Some(Keyword::SCHEMA) => Some(GrantObjects::Schemas(objects?)), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 27fe09c7..1de1d93f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1232,7 +1232,6 @@ fn parse_select_expr_star() { "SELECT 2. * 3 FROM T", ); dialects.verified_only_select("SELECT myfunc().* FROM T"); - dialects.verified_only_select("SELECT myfunc().* EXCEPT (foo) FROM T"); // Invalid let res = dialects.parse_sql_statements("SELECT foo.*.* FROM T"); @@ -1240,6 +1239,11 @@ fn parse_select_expr_star() { ParserError::ParserError("Expected: end of statement, found: .".to_string()), res.unwrap_err() ); + + let dialects = all_dialects_where(|d| { + d.supports_select_expr_star() && d.supports_select_wildcard_except() + }); + dialects.verified_only_select("SELECT myfunc().* EXCEPT (foo) FROM T"); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e7393d3f..6081e318 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4232,3 +4232,138 @@ fn test_snowflake_create_view_with_composite_policy_name() { r#"CREATE VIEW X (COL WITH MASKING POLICY foo.bar.baz) AS SELECT * FROM Y"#; snowflake().verified_stmt(create_view_with_tag); } + +#[test] +fn test_snowflake_identifier_function() { + // Using IDENTIFIER to reference a column + match &snowflake() + .verified_only_select("SELECT identifier('email') FROM customers") + .projection[0] + { + SelectItem::UnnamedExpr(Expr::Function(Function { name, args, .. })) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("email".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a case-sensitive column + match &snowflake() + .verified_only_select(r#"SELECT identifier('"Email"') FROM customers"#) + .projection[0] + { + SelectItem::UnnamedExpr(Expr::Function(Function { name, args, .. })) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("\"Email\"".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference an alias of a table + match &snowflake() + .verified_only_select("SELECT identifier('alias1').* FROM tbl AS alias1") + .projection[0] + { + SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(Expr::Function(Function { name, args, .. })), + _, + ) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("alias1".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a database + match snowflake().verified_stmt("CREATE DATABASE IDENTIFIER('tbl')") { + Statement::CreateDatabase { db_name, .. } => { + assert_eq!( + db_name, + ObjectName(vec![ObjectNamePart::Function(ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("tbl".to_string()).into() + )))] + })]) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a schema + match snowflake().verified_stmt("CREATE SCHEMA IDENTIFIER('db1.sc1')") { + Statement::CreateSchema { schema_name, .. } => { + assert_eq!( + schema_name, + SchemaName::Simple(ObjectName(vec![ObjectNamePart::Function( + ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("db1.sc1".to_string()).into() + )))] + } + )])) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a table + match snowflake().verified_stmt("CREATE TABLE IDENTIFIER('tbl') (id INT)") { + Statement::CreateTable(CreateTable { name, .. }) => { + assert_eq!( + name, + ObjectName(vec![ObjectNamePart::Function(ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("tbl".to_string()).into() + )))] + })]) + ); + } + _ => unreachable!(), + } + + // Cannot have more than one IDENTIFIER part in an object name + assert_eq!( + snowflake() + .parse_sql_statements( + "CREATE TABLE IDENTIFIER('db1').IDENTIFIER('sc1').IDENTIFIER('tbl') (id INT)" + ) + .is_err(), + true + ); + assert_eq!( + snowflake() + .parse_sql_statements("CREATE TABLE IDENTIFIER('db1')..IDENTIFIER('tbl') (id INT)") + .is_err(), + true + ); +} From 8f1414efffa530af17c0853a8de4f0f0c73edacc Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:28:51 +0200 Subject: [PATCH 126/130] Add support for granting privileges to procedures and functions in Snowflake (#1930) --- src/ast/mod.rs | 33 +++++++++++++++++++++++++++++++++ src/parser/mod.rs | 34 ++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 2 ++ 3 files changed, 69 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 619730e2..75e88f8a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7023,6 +7023,25 @@ pub enum GrantObjects { ReplicationGroup(Vec), /// Grant privileges on external volumes ExternalVolumes(Vec), + /// Grant privileges on a procedure. In dialects that + /// support overloading, the argument types must be specified. + /// + /// For example: + /// `GRANT USAGE ON PROCEDURE foo(varchar) TO ROLE role1` + Procedure { + name: ObjectName, + arg_types: Vec, + }, + + /// Grant privileges on a function. In dialects that + /// support overloading, the argument types must be specified. + /// + /// For example: + /// `GRANT USAGE ON FUNCTION foo(varchar) TO ROLE role1` + Function { + name: ObjectName, + arg_types: Vec, + }, } impl fmt::Display for GrantObjects { @@ -7147,6 +7166,20 @@ impl fmt::Display for GrantObjects { GrantObjects::ExternalVolumes(objects) => { write!(f, "EXTERNAL VOLUME {}", display_comma_separated(objects)) } + GrantObjects::Procedure { name, arg_types } => { + write!(f, "PROCEDURE {name}")?; + if !arg_types.is_empty() { + write!(f, "({})", display_comma_separated(arg_types))?; + } + Ok(()) + } + GrantObjects::Function { name, arg_types } => { + write!(f, "FUNCTION {name}")?; + if !arg_types.is_empty() { + write!(f, "({})", display_comma_separated(arg_types))?; + } + Ok(()) + } } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5a42bb9e..b00cd16d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14061,6 +14061,8 @@ impl<'a> Parser<'a> { Keyword::INTEGRATION, Keyword::USER, Keyword::CONNECTION, + Keyword::PROCEDURE, + Keyword::FUNCTION, ]); let objects = self.parse_comma_separated(|p| p.parse_object_name_inner(false, true)); @@ -14073,6 +14075,13 @@ impl<'a> Parser<'a> { Some(Keyword::VIEW) => Some(GrantObjects::Views(objects?)), Some(Keyword::USER) => Some(GrantObjects::Users(objects?)), Some(Keyword::CONNECTION) => Some(GrantObjects::Connections(objects?)), + kw @ (Some(Keyword::PROCEDURE) | Some(Keyword::FUNCTION)) => { + if let Some(name) = objects?.first() { + self.parse_grant_procedure_or_function(name, &kw)? + } else { + self.expected("procedure or function name", self.peek_token())? + } + } Some(Keyword::TABLE) | None => Some(GrantObjects::Tables(objects?)), _ => unreachable!(), } @@ -14084,6 +14093,31 @@ impl<'a> Parser<'a> { Ok((privileges, objects)) } + fn parse_grant_procedure_or_function( + &mut self, + name: &ObjectName, + kw: &Option, + ) -> Result, ParserError> { + let arg_types = if self.consume_token(&Token::LParen) { + let list = self.parse_comma_separated0(Self::parse_data_type, Token::RParen)?; + self.expect_token(&Token::RParen)?; + list + } else { + vec![] + }; + match kw { + Some(Keyword::PROCEDURE) => Ok(Some(GrantObjects::Procedure { + name: name.clone(), + arg_types, + })), + Some(Keyword::FUNCTION) => Ok(Some(GrantObjects::Function { + name: name.clone(), + arg_types, + })), + _ => self.expected("procedure or function keywords", self.peek_token())?, + } + } + pub fn parse_grant_permission(&mut self) -> Result { fn parse_columns(parser: &mut Parser) -> Result>, ParserError> { let columns = parser.parse_parenthesized_column_list(Optional, false)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1de1d93f..a00405aa 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9460,6 +9460,8 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON FUTURE VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT SELECT ON FUTURE MATERIALIZED VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT SELECT ON FUTURE SEQUENCES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT USAGE ON PROCEDURE db1.sc1.foo(INT) TO ROLE role1"); + verified_stmt("GRANT USAGE ON FUNCTION db1.sc1.foo(INT) TO ROLE role1"); } #[test] From fd4934ec74ab23cab6ef89750dc161bdeceff25e Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:01:08 +0200 Subject: [PATCH 127/130] Add support for `+` char in Snowflake stage names (#1935) --- src/dialect/snowflake.rs | 1 + tests/sqlparser_snowflake.rs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index e005300e..21bc9593 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -735,6 +735,7 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result ident.push('~'), Token::Mod => ident.push('%'), Token::Div => ident.push('/'), + Token::Plus => ident.push('+'), Token::Word(w) => ident.push_str(&w.to_string()), _ => return parser.expected("stage name identifier", parser.peek_token()), } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 6081e318..389f4703 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2581,6 +2581,26 @@ fn test_snowflake_copy_into() { } _ => unreachable!(), } + + // Test for non-ident characters in stage names + let sql = "COPY INTO a.b FROM @namespace.stage_name/x@x~x%x+"; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { into, from_obj, .. } => { + assert_eq!( + into, + ObjectName::from(vec![Ident::new("a"), Ident::new("b")]) + ); + assert_eq!( + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@namespace"), + Ident::new("stage_name/x@x~x%x+") + ])) + ) + } + _ => unreachable!(), + } } #[test] From 15f35e14765e58724e37704818a8c16426f2f145 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:13:20 +0200 Subject: [PATCH 128/130] Snowflake Reserved SQL Keywords as Implicit Table Alias (#1934) --- src/dialect/mod.rs | 10 +++- src/dialect/snowflake.rs | 88 ++++++++++++++++++++++++++++++++++-- tests/sqlparser_common.rs | 26 ++++++++--- tests/sqlparser_snowflake.rs | 51 +++++++++++++++++++++ 4 files changed, 163 insertions(+), 12 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 8f9dd617..861dfe26 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -992,11 +992,17 @@ pub trait Dialect: Debug + Any { explicit || self.is_column_alias(kw, parser) } + /// Returns true if the specified keyword should be parsed as a table identifier. + /// See [keywords::RESERVED_FOR_TABLE_ALIAS] + fn is_table_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) + } + /// Returns true if the specified keyword should be parsed as a table factor alias. /// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided /// to enable looking ahead if needed. - fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { - explicit || !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) + fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + explicit || self.is_table_alias(kw, parser) } /// Returns true if this dialect supports querying historical table data diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 21bc9593..85c09acb 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -318,9 +318,11 @@ impl Dialect for SnowflakeDialect { } // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` - // which would give it a different meanings, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias - Keyword::FETCH - if parser.peek_keyword(Keyword::FIRST) || parser.peek_keyword(Keyword::NEXT) => + // which would give it a different meanings, for example: + // `SELECT 1 FETCH FIRST 10 ROWS` - not an alias + // `SELECT 1 FETCH 10` - not an alias + Keyword::FETCH if parser.peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]).is_some() + || matches!(parser.peek_token().token, Token::Number(_, _)) => { false } @@ -345,6 +347,86 @@ impl Dialect for SnowflakeDialect { } } + fn is_table_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool { + match kw { + // The following keywords can be considered an alias as long as + // they are not followed by other tokens that may change their meaning + Keyword::LIMIT + | Keyword::RETURNING + | Keyword::INNER + | Keyword::USING + | Keyword::PIVOT + | Keyword::UNPIVOT + | Keyword::EXCEPT + | Keyword::MATCH_RECOGNIZE + | Keyword::OFFSET + if !matches!(parser.peek_token_ref().token, Token::SemiColon | Token::EOF) => + { + false + } + + // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` + // which would give it a different meanings, for example: + // `SELECT * FROM tbl FETCH FIRST 10 ROWS` - not an alias + // `SELECT * FROM tbl FETCH 10` - not an alias + Keyword::FETCH + if parser + .peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]) + .is_some() + || matches!(parser.peek_token().token, Token::Number(_, _)) => + { + false + } + + // All sorts of join-related keywords can be considered aliases unless additional + // keywords change their meaning. + Keyword::RIGHT | Keyword::LEFT | Keyword::SEMI | Keyword::ANTI + if parser + .peek_one_of_keywords(&[Keyword::JOIN, Keyword::OUTER]) + .is_some() => + { + false + } + Keyword::GLOBAL if parser.peek_keyword(Keyword::FULL) => false, + + // Reserved keywords by the Snowflake dialect, which seem to be less strictive + // than what is listed in `keywords::RESERVED_FOR_TABLE_ALIAS`. The following + // keywords were tested with the this statement: `SELECT .* FROM tbl `. + Keyword::WITH + | Keyword::ORDER + | Keyword::SELECT + | Keyword::WHERE + | Keyword::GROUP + | Keyword::HAVING + | Keyword::LATERAL + | Keyword::UNION + | Keyword::INTERSECT + | Keyword::MINUS + | Keyword::ON + | Keyword::JOIN + | Keyword::INNER + | Keyword::CROSS + | Keyword::FULL + | Keyword::LEFT + | Keyword::RIGHT + | Keyword::NATURAL + | Keyword::USING + | Keyword::ASOF + | Keyword::MATCH_CONDITION + | Keyword::SET + | Keyword::QUALIFY + | Keyword::FOR + | Keyword::START + | Keyword::CONNECT + | Keyword::SAMPLE + | Keyword::TABLESAMPLE + | Keyword::FROM => false, + + // Any other word is considered an alias + _ => true, + } + } + /// See: fn supports_timestamp_versioning(&self) -> bool { true diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a00405aa..906be9be 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5548,7 +5548,8 @@ fn parse_named_window_functions() { WINDOW w AS (PARTITION BY x), win AS (ORDER BY y)"; supported_dialects.verified_stmt(sql); - let select = verified_only_select(sql); + let select = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))) + .verified_only_select(sql); const EXPECTED_PROJ_QTY: usize = 2; assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); @@ -5578,6 +5579,7 @@ fn parse_named_window_functions() { #[test] fn parse_window_clause() { + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))); let sql = "SELECT * \ FROM mytable \ WINDOW \ @@ -5590,10 +5592,14 @@ fn parse_window_clause() { window7 AS (window1 ROWS UNBOUNDED PRECEDING), \ window8 AS (window1 PARTITION BY a ORDER BY b ROWS UNBOUNDED PRECEDING) \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); let sql = "SELECT * from mytable WINDOW window1 AS window2"; - let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let dialects = all_dialects_except(|d| { + d.is::() + || d.is::() + || d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)) + }); let res = dialects.parse_sql_statements(sql); assert_eq!( ParserError::ParserError("Expected: (, found: window2".to_string()), @@ -5603,6 +5609,7 @@ fn parse_window_clause() { #[test] fn test_parse_named_window() { + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))); let sql = "SELECT \ MIN(c12) OVER window1 AS min1, \ MAX(c12) OVER window2 AS max1 \ @@ -5610,7 +5617,7 @@ fn test_parse_named_window() { WINDOW window1 AS (ORDER BY C12), \ window2 AS (PARTITION BY C11) \ ORDER BY C3"; - let actual_select_only = verified_only_select(sql); + let actual_select_only = dialects.verified_only_select(sql); let expected = Select { select_token: AttachedToken::empty(), distinct: None, @@ -5759,6 +5766,10 @@ fn test_parse_named_window() { #[test] fn parse_window_and_qualify_clause() { + let dialects = all_dialects_except(|d| { + d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)) + || d.is_table_alias(&Keyword::QUALIFY, &mut Parser::new(d)) + }); let sql = "SELECT \ MIN(c12) OVER window1 AS min1 \ FROM aggregate_test_100 \ @@ -5766,7 +5777,7 @@ fn parse_window_and_qualify_clause() { WINDOW window1 AS (ORDER BY C12), \ window2 AS (PARTITION BY C11) \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); let sql = "SELECT \ MIN(c12) OVER window1 AS min1 \ @@ -5775,7 +5786,7 @@ fn parse_window_and_qualify_clause() { window2 AS (PARTITION BY C11) \ QUALIFY ROW_NUMBER() OVER my_window \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); } #[test] @@ -7443,7 +7454,8 @@ fn parse_join_syntax_variants() { "SELECT c1 FROM t1 FULL JOIN t2 USING(c1)", ); - let res = parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1"); + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::OUTER, &mut Parser::new(d))); + let res = dialects.parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1"); assert_eq!( ParserError::ParserError("Expected: APPLY, found: JOIN".to_string()), res.unwrap_err() diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 389f4703..65546bee 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3492,6 +3492,57 @@ fn test_sql_keywords_as_select_item_aliases() { } } +#[test] +fn test_sql_keywords_as_table_aliases() { + // Some keywords that should be parsed as an alias implicitly + let unreserved_kws = vec![ + "VIEW", + "EXPLAIN", + "ANALYZE", + "SORT", + "PIVOT", + "UNPIVOT", + "TOP", + "LIMIT", + "OFFSET", + "FETCH", + "EXCEPT", + "CLUSTER", + "DISTRIBUTE", + "GLOBAL", + "ANTI", + "SEMI", + "RETURNING", + "OUTER", + "WINDOW", + "END", + "PARTITION", + "PREWHERE", + "SETTINGS", + "FORMAT", + "MATCH_RECOGNIZE", + "OPEN", + ]; + + for kw in unreserved_kws { + snowflake().verified_stmt(&format!("SELECT * FROM tbl AS {kw}")); + snowflake().one_statement_parses_to( + &format!("SELECT * FROM tbl {kw}"), + &format!("SELECT * FROM tbl AS {kw}"), + ); + } + + // Some keywords that should not be parsed as an alias implicitly + let reserved_kws = vec![ + "FROM", "GROUP", "HAVING", "ORDER", "SELECT", "UNION", "WHERE", "WITH", + ]; + for kw in reserved_kws { + assert!(snowflake() + .parse_sql_statements(&format!("SELECT * FROM tbl {kw}")) + .is_err()); + } +} + #[test] fn test_timetravel_at_before() { snowflake().verified_only_select("SELECT * FROM tbl AT(TIMESTAMP => '2024-12-15 00:00:00')"); From ee31b64f9e9c0959aa6fcada159d8092517966e3 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:39:29 +0200 Subject: [PATCH 129/130] Add support for Redshift `SELECT * EXCLUDE` (#1936) --- src/ast/query.rs | 9 +++ src/ast/spans.rs | 1 + src/dialect/duckdb.rs | 4 ++ src/dialect/generic.rs | 4 ++ src/dialect/mod.rs | 20 ++++++ src/dialect/redshift.rs | 8 +++ src/dialect/snowflake.rs | 4 ++ src/keywords.rs | 1 + src/parser/mod.rs | 11 +++- tests/sqlparser_clickhouse.rs | 1 + tests/sqlparser_common.rs | 115 ++++++++++++++++++++++++++++++++++ tests/sqlparser_duckdb.rs | 2 + tests/sqlparser_mssql.rs | 3 + tests/sqlparser_mysql.rs | 9 ++- tests/sqlparser_postgres.rs | 3 + 15 files changed, 192 insertions(+), 3 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index febf1fc6..7ffb64d9 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -321,6 +321,11 @@ pub struct Select { pub top_before_distinct: bool, /// projection expressions pub projection: Vec, + /// 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, /// INTO pub into: Option, /// FROM @@ -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)?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a1b2e4e0..3e82905e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2220,6 +2220,7 @@ impl Spanned for Select { distinct: _, // todo top: _, // todo, mysql specific projection, + exclude: _, into, from, lateral_views, diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 3366c670..fa18463a 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -94,4 +94,8 @@ impl Dialect for DuckDbDialect { fn supports_order_by_all(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 5e9f2e4e..be2cc007 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -179,4 +179,8 @@ impl Dialect for GenericDialect { fn supports_filter_during_aggregation(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 861dfe26..deb5719d 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -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. diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 9ad9c5fd..8ffed98a 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -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 + } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 85c09acb..3b1eff39 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -466,6 +466,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 { diff --git a/src/keywords.rs b/src/keywords.rs index 73865150..9e689a6d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -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, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b00cd16d..a2d01f13 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11740,6 +11740,7 @@ impl<'a> Parser<'a> { top: None, top_before_distinct: false, projection: vec![], + exclude: None, into: None, from, lateral_views: vec![], @@ -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 { @@ -11915,6 +11922,7 @@ impl<'a> Parser<'a> { top, top_before_distinct, projection, + exclude, into, from, lateral_views, @@ -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 { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 1d8669a2..9e5b6ce8 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -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")])), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 906be9be..b7b5b630 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -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")])), @@ -5695,6 +5696,7 @@ fn test_parse_named_window() { }, }, ], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -6351,6 +6353,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 { @@ -8620,6 +8623,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 { @@ -9616,6 +9620,7 @@ fn parse_merge() { projection: vec![SelectItem::Wildcard( WildcardAdditionalOptions::default() )], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![ @@ -11534,6 +11539,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")])), @@ -11734,6 +11740,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![], @@ -11815,6 +11822,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![], @@ -12748,6 +12756,7 @@ fn test_extract_seconds_ok() { format: None, }), })], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -14820,6 +14829,7 @@ fn test_select_from_first() { distinct: None, top: None, projection, + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { @@ -16000,3 +16010,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()); + 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()) + ); +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 371d3aac..fe14b7ba 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -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 { @@ -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 { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ebbec25f..b4a650ce 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -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![], @@ -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 { @@ -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![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 44f36584..e8c6719e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1403,6 +1403,7 @@ fn parse_escaped_quote_identifiers_with_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1456,6 +1457,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1503,6 +1505,7 @@ fn parse_escaped_backticks_with_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1554,6 +1557,7 @@ fn parse_escaped_backticks_with_no_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2225,6 +2229,7 @@ fn parse_select_with_numeric_prefix_column_name() { projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new( "123col_$@123abc" )))], + exclude: None, into: None, from: vec![TableWithJoins { 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, Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), - distinct: None, top: None, 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::Identifier(Ident::new("123col_$@123abc"))) ], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::with_quote( @@ -3043,6 +3048,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 { @@ -3357,6 +3363,7 @@ fn parse_hex_string_introducer() { ) .into(), })], + exclude: None, from: vec![], lateral_views: vec![], prewhere: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 48792025..0d1d138c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1305,6 +1305,7 @@ fn parse_copy_to() { }, } ], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2948,6 +2949,7 @@ fn parse_array_subquery_expr() { projection: vec![SelectItem::UnnamedExpr(Expr::Value( (number("1")).with_empty_span() ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2973,6 +2975,7 @@ fn parse_array_subquery_expr() { projection: vec![SelectItem::UnnamedExpr(Expr::Value( (number("2")).with_empty_span() ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], From bc2c4e263d85a2440c87ea56f0c595a458e40fc8 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:46:48 +0200 Subject: [PATCH 130/130] Support optional semicolon between statements (#1937) --- src/parser/mod.rs | 8 ++++++++ src/test_utils.rs | 5 +++++ tests/sqlparser_common.rs | 22 ++++++++++++++++++++-- tests/sqlparser_mssql.rs | 26 +++++++++++++++++++++++++- tests/sqlparser_mysql.rs | 1 + 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a2d01f13..47b63da8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -222,6 +222,9 @@ pub struct ParserOptions { /// Controls how literal values are unescaped. See /// [`Tokenizer::with_unescape`] for more details. pub unescape: bool, + /// Controls if the parser expects a semi-colon token + /// between statements. Default is `true`. + pub require_semicolon_stmt_delimiter: bool, } impl Default for ParserOptions { @@ -229,6 +232,7 @@ impl Default for ParserOptions { Self { trailing_commas: false, unescape: true, + require_semicolon_stmt_delimiter: true, } } } @@ -467,6 +471,10 @@ impl<'a> Parser<'a> { expecting_statement_delimiter = false; } + if !self.options.require_semicolon_stmt_delimiter { + expecting_statement_delimiter = false; + } + match self.peek_token().token { Token::EOF => break, diff --git a/src/test_utils.rs b/src/test_utils.rs index 544ceaef..654f2723 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -294,6 +294,11 @@ pub fn all_dialects() -> TestedDialects { ]) } +// Returns all available dialects with the specified parser options +pub fn all_dialects_with_options(options: ParserOptions) -> TestedDialects { + TestedDialects::new_with_options(all_dialects().dialects, options) +} + /// Returns all dialects matching the given predicate. pub fn all_dialects_where(predicate: F) -> TestedDialects where diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b7b5b630..15144479 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -40,8 +40,9 @@ use sqlparser::parser::{Parser, ParserError, ParserOptions}; use sqlparser::tokenizer::Tokenizer; use sqlparser::tokenizer::{Location, Span}; use test_utils::{ - all_dialects, all_dialects_where, alter_table_op, assert_eq_vec, call, expr_from_projection, - join, number, only, table, table_alias, table_from_name, TestedDialects, + all_dialects, all_dialects_where, all_dialects_with_options, alter_table_op, assert_eq_vec, + call, expr_from_projection, join, number, only, table, table_alias, table_from_name, + TestedDialects, }; #[macro_use] @@ -16115,3 +16116,20 @@ fn test_select_exclude() { ParserError::ParserError("Expected: end of statement, found: EXCLUDE".to_string()) ); } + +#[test] +fn test_no_semicolon_required_between_statements() { + let sql = r#" +SELECT * FROM tbl1 +SELECT * FROM tbl2 + "#; + + let dialects = all_dialects_with_options(ParserOptions { + trailing_commas: false, + unescape: true, + require_semicolon_stmt_delimiter: false, + }); + let stmts = dialects.parse_sql_statements(sql).unwrap(); + assert_eq!(stmts.len(), 2); + assert!(stmts.iter().all(|s| matches!(s, Statement::Query { .. }))); +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b4a650ce..50c6448d 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -32,7 +32,7 @@ use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MsSqlDialect}; -use sqlparser::parser::{Parser, ParserError}; +use sqlparser::parser::{Parser, ParserError, ParserOptions}; #[test] fn parse_mssql_identifiers() { @@ -2327,6 +2327,18 @@ fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } +// MS SQL dialect with support for optional semi-colon statement delimiters +fn tsql() -> TestedDialects { + TestedDialects::new_with_options( + vec![Box::new(MsSqlDialect {})], + ParserOptions { + trailing_commas: false, + unescape: true, + require_semicolon_stmt_delimiter: false, + }, + ) +} + fn ms_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } @@ -2483,3 +2495,15 @@ fn parse_mssql_grant() { fn parse_mssql_deny() { ms().verified_stmt("DENY SELECT ON my_table TO public, db_admin"); } + +#[test] +fn test_tsql_no_semicolon_delimiter() { + let sql = r#" +DECLARE @X AS NVARCHAR(MAX)='x' +DECLARE @Y AS NVARCHAR(MAX)='y' + "#; + + let stmts = tsql().parse_sql_statements(sql).unwrap(); + assert_eq!(stmts.len(), 2); + assert!(stmts.iter().all(|s| matches!(s, Statement::Declare { .. }))); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e8c6719e..9068ed9c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1442,6 +1442,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { ParserOptions { trailing_commas: false, unescape: false, + require_semicolon_stmt_delimiter: true, } ) .verified_stmt(sql),